Compare commits
244 Commits
v2.0.10-be
...
v2.0.23-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522684b2ab | ||
|
|
feab16b351 | ||
|
|
ee041db63d | ||
|
|
2479533d07 | ||
|
|
d045fd5834 | ||
|
|
8407f27fed | ||
|
|
b505286caf | ||
|
|
feb762ce8b | ||
|
|
8acdb5af83 | ||
|
|
5af1294f71 | ||
|
|
87d2d273d3 | ||
|
|
b5c52ac71e | ||
|
|
efe9a15f72 | ||
|
|
525f1e4b0b | ||
|
|
d18820b832 | ||
|
|
7e024fd736 | ||
|
|
c9c5989474 | ||
|
|
ce9f96d3be | ||
|
|
7362dd0bf4 | ||
|
|
9905ebc144 | ||
|
|
8f8010884b | ||
|
|
37afd141be | ||
|
|
a3643b4302 | ||
|
|
02cfd8d9b7 | ||
|
|
941ce439b4 | ||
|
|
a08bce2073 | ||
|
|
4e9c8322c3 | ||
|
|
89bfe85be3 | ||
|
|
98d994591c | ||
|
|
a29bc7f4f9 | ||
|
|
288f4c5f7f | ||
|
|
a6bf78ed56 | ||
|
|
8dbb05931e | ||
|
|
ac8a712ff0 | ||
|
|
39406c25c3 | ||
|
|
48d7c2c54c | ||
|
|
0217188274 | ||
|
|
fd762e71de | ||
|
|
4d5c3b6df0 | ||
|
|
7df54e4d1b | ||
|
|
5d085de9d3 | ||
|
|
a8a4299086 | ||
|
|
86f0e8425c | ||
|
|
d2e879be4a | ||
|
|
544114fffe | ||
|
|
3b3e207b11 | ||
|
|
84aad638ac | ||
|
|
2bb691966e | ||
|
|
8f5e788270 | ||
|
|
7c43ea2f46 | ||
|
|
8146e1e3cf | ||
|
|
51b1ff6d4a | ||
|
|
403e8dfbea | ||
|
|
9d08717c83 | ||
|
|
66167d5960 | ||
|
|
624863d826 | ||
|
|
d4b3810fbc | ||
|
|
6056e1d3b9 | ||
|
|
1a293d525f | ||
|
|
b87eb68bdd | ||
|
|
8620546d07 | ||
|
|
a082109045 | ||
|
|
559a9b393e | ||
|
|
ae41b22e59 | ||
|
|
754fd24421 | ||
|
|
ab34a74210 | ||
|
|
cfa6de4d91 | ||
|
|
a5608c7a1e | ||
|
|
88a7b52e51 | ||
|
|
e444bad4de | ||
|
|
5403b0b547 | ||
|
|
51b5e615f5 | ||
|
|
700547b63b | ||
|
|
3f3d1962c7 | ||
|
|
655a359ef4 | ||
|
|
90647628c9 | ||
|
|
681c3ed6e3 | ||
|
|
7f255943c6 | ||
|
|
b6e73b5dea | ||
|
|
eacb7f6ae5 | ||
|
|
7b300bb87e | ||
|
|
a81ad27d85 | ||
|
|
8eed14ff3b | ||
|
|
82446acdf0 | ||
|
|
88770b8805 | ||
|
|
f9f05bbea3 | ||
|
|
17dd767c22 | ||
|
|
25b1dc6dd8 | ||
|
|
b2b1277e37 | ||
|
|
8e1a588ced | ||
|
|
9eddfafeae | ||
|
|
d24a922ccb | ||
|
|
bbc6482c99 | ||
|
|
36ff1fb674 | ||
|
|
f0aa793262 | ||
|
|
681627a656 | ||
|
|
87c6ad66fb | ||
|
|
4ab9eb3bfa | ||
|
|
2d56ac027b | ||
|
|
836c4293d6 | ||
|
|
07092e8aa5 | ||
|
|
66743c1401 | ||
|
|
bfe34e060b | ||
|
|
5ed4236a22 | ||
|
|
868aeb3902 | ||
|
|
cbcdac5b04 | ||
|
|
d473bb3058 | ||
|
|
066a95d209 | ||
|
|
c7cc476623 | ||
|
|
bd44eb7fe4 | ||
|
|
6ec4f51077 | ||
|
|
b4a4f60b04 | ||
|
|
dc4e6edc9a | ||
|
|
60b362b19e | ||
|
|
7e81ce8c06 | ||
|
|
c7f9e2f721 | ||
|
|
cab8b1c041 | ||
|
|
16f270691d | ||
|
|
d94a1efe75 | ||
|
|
12755970b7 | ||
|
|
93e4853ea2 | ||
|
|
5e0c0365fb | ||
|
|
c2713c53dd | ||
|
|
90443b4028 | ||
|
|
e0109ed179 | ||
|
|
a53afe05a2 | ||
|
|
a5d2467bfe | ||
|
|
8447663e27 | ||
|
|
64d67d8209 | ||
|
|
78034b82a9 | ||
|
|
f77bd6c17b | ||
|
|
2621da7d36 | ||
|
|
e1dca1509a | ||
|
|
df016243dd | ||
|
|
be72693fec | ||
|
|
33a1ebdb1a | ||
|
|
030f9d334b | ||
|
|
dc743ac378 | ||
|
|
0010cbe21f | ||
|
|
3a5d5918de | ||
|
|
3380e39de2 | ||
|
|
7d31079897 | ||
|
|
c287b6df77 | ||
|
|
dab1f8ba20 | ||
|
|
a26de7f6c2 | ||
|
|
ab32b2cbc2 | ||
|
|
503c249fc3 | ||
|
|
2a03ce757e | ||
|
|
373a15524f | ||
|
|
13036183d3 | ||
|
|
170591c79e | ||
|
|
a15d225a5f | ||
|
|
a0106874e2 | ||
|
|
ab157d1c0e | ||
|
|
0b95c9fe2e | ||
|
|
d693514ca9 | ||
|
|
56987b3aaa | ||
|
|
3ca1bd5d78 | ||
|
|
5d2219f2f8 | ||
|
|
56dc28eed3 | ||
|
|
3e723d4373 | ||
|
|
f5e341e655 | ||
|
|
3c81100957 | ||
|
|
304378f93b | ||
|
|
de6b6e8124 | ||
|
|
d15223fb1a | ||
|
|
d29a12b6db | ||
|
|
9100e25a21 | ||
|
|
7672f1955e | ||
|
|
5f52171fc4 | ||
|
|
31ac82ad71 | ||
|
|
38ca4e37a6 | ||
|
|
3c55550702 | ||
|
|
7dff6b121b | ||
|
|
d77d889695 | ||
|
|
318a21438f | ||
|
|
7175b57a28 | ||
|
|
e1e5a050c2 | ||
|
|
58996c1115 | ||
|
|
7301fe5f6e | ||
|
|
a27c423569 | ||
|
|
19680d3bc7 | ||
|
|
ecaca4e5dc | ||
|
|
191de0b577 | ||
|
|
ebcc073b32 | ||
|
|
043b3fd57b | ||
|
|
dd50502dcb | ||
|
|
f159a1014d | ||
|
|
abb801535c | ||
|
|
2732dbf1b1 | ||
|
|
095d893005 | ||
|
|
5d8455d141 | ||
|
|
aa3450bfcc | ||
|
|
770f12b632 | ||
|
|
45c2ccdffe | ||
|
|
fc14c3165f | ||
|
|
0fad245148 | ||
|
|
79609c384e | ||
|
|
09054ddb4b | ||
|
|
6f912d4aa2 | ||
|
|
96033a8214 | ||
|
|
5ca65f4797 | ||
|
|
d2fccbde68 | ||
|
|
e6b48d7baf | ||
|
|
3e51310511 | ||
|
|
32b43202c2 | ||
|
|
446170f8de | ||
|
|
c5a9ecd4ac | ||
|
|
2af5f817a3 | ||
|
|
4e55cf3cd4 | ||
|
|
eeb0478813 | ||
|
|
33739f1cb2 | ||
|
|
515e6a8071 | ||
|
|
2b22f8eb4f | ||
|
|
e9725a0081 | ||
|
|
8fd159d2fe | ||
|
|
3d7e6c8b2c | ||
|
|
0c048d61b1 | ||
|
|
f05b8e5cd1 | ||
|
|
0b38fec827 | ||
|
|
547dc9ed33 | ||
|
|
896a37bea9 | ||
|
|
3f90037db3 | ||
|
|
380ca11ced | ||
|
|
ab3a288e49 | ||
|
|
638e225f80 | ||
|
|
5089ede207 | ||
|
|
a3e6e76158 | ||
|
|
7c4c7bfc90 | ||
|
|
644fea6665 | ||
|
|
a1349ff8a6 | ||
|
|
71c20002b8 | ||
|
|
157af84226 | ||
|
|
9b4536f132 | ||
|
|
29ab470e42 | ||
|
|
c67fa480a7 | ||
|
|
0a1a691c73 | ||
|
|
48588f23bf | ||
|
|
cf14fbc3f0 | ||
|
|
e471d5207d | ||
|
|
5722a52082 | ||
|
|
08c32e875e | ||
|
|
7d3ee3afb3 | ||
|
|
def8600f5c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@
|
|||||||
*.db*
|
*.db*
|
||||||
*.db-journal
|
*.db-journal
|
||||||
*.ini
|
*.ini
|
||||||
|
release.lock
|
||||||
version.lock
|
version.lock
|
||||||
logs/*
|
logs/*
|
||||||
cache/*
|
cache/*
|
||||||
|
|||||||
198
CHANGELOG.md
198
CHANGELOG.md
@@ -1,5 +1,203 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.23-beta (2018-03-16)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Certain transcode stream showing incorrectly as direct play in history. Fix is not retroactive.
|
||||||
|
* Notifications:
|
||||||
|
* New: Added season/episode/album/track count to notification parameters.
|
||||||
|
* New: Added "Value 3" setting for IFTTT notifications.
|
||||||
|
* New: Set PLEX_URL, PLEX_TOKEN, TAUTULLI_URL, and TAUTULLI_APIKEY environment variables for scripts.
|
||||||
|
* Fix: Notifications failing to send with invalid custom conditions json.
|
||||||
|
* Fix: Email notifications failing with unicode username/passwords.
|
||||||
|
* Change: Facebook Graph API version updated to v2.12.
|
||||||
|
* UI:
|
||||||
|
* New: Show the Plex Server URL in the settings.
|
||||||
|
* Fix: Incorrect info displayed in the Tautulli login logs.
|
||||||
|
* API:
|
||||||
|
* Fix: API returning empty data if a message was in the original data.
|
||||||
|
* Change: get_server_id command returns json instead of string.
|
||||||
|
* Other:
|
||||||
|
* Fix: Forgot git pull when changing branches in the web UI.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.22 (2018-03-10)
|
||||||
|
|
||||||
|
* Tautulli v2 release!
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.22-beta (2018-03-09)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Pushover notifications failing with priority 2 is set.
|
||||||
|
* Fix: Expanding selectize box for some notification agent settings.
|
||||||
|
* Other:
|
||||||
|
* Fix: Update check failing when an update is available.
|
||||||
|
* Fix: Item count incorrect for photo libraries.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.21-beta (2018-03-04)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* New: Identify if a stream is using Plex Relay.
|
||||||
|
* Change: Don't ping the Plex server if the websocket is disconnected.
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Pause/resume state not being sent correctly in some instances.
|
||||||
|
* Other:
|
||||||
|
* New: Add Patreon donation method.
|
||||||
|
* Fix: Catch failure to send analytics.
|
||||||
|
* Fix: IP address connection lookup error when the country is missing.
|
||||||
|
* Change: Updated all init scripts to Tautulli.
|
||||||
|
* Change: Move database to tautulli.db.
|
||||||
|
* Change: Move logs to tautulli.log.
|
||||||
|
* Change: Move startup file to Tautulli.py.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.20-beta (2018-02-24)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* New: Add poster support for Pushover notifications.
|
||||||
|
* New: Add poster support for Pushbullet notifications.
|
||||||
|
* Fix: Incorrect Plex/Tautulli update notification parameter types.
|
||||||
|
* Change: Poster and text sent as a single message for Telegram.
|
||||||
|
* Change: Posters uploaded directly to Telegram without Imgur.
|
||||||
|
* UI:
|
||||||
|
* New: Add "Delete" button to synced items table on user pages.
|
||||||
|
* Fix: Button spacing/positioning on mobile site.
|
||||||
|
* Fix: Music statistic cards not using the fallback thumbnail.
|
||||||
|
* Fix: Logo not showing up when using an SVG.
|
||||||
|
* Change: Graphs now respect the "Group History" setting.
|
||||||
|
* API:
|
||||||
|
* New: Add grouping to graph API commands.
|
||||||
|
* Other:
|
||||||
|
* New: Added Google Analytics to collect installation metrics.
|
||||||
|
* Fix: Reconnecting to the Plex server when server settings are not changed.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.19-beta (2018-02-16)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Connect to Plex Cloud server without keeping it awake.
|
||||||
|
* Fix: Reconnect to Plex Cloud server after the server wakes up from sleeping.
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Don't send Plex Server Up/Down notifications when Tautulli starts up.
|
||||||
|
* Change: Better handling of Watched notifications.
|
||||||
|
* UI:
|
||||||
|
* New: Added Plex server selection dropdown in the settings.
|
||||||
|
* Fix: Libraries and Users tables not refreshing properly.
|
||||||
|
* Change: Updated the masked info shown to guests.
|
||||||
|
* Change: Check for updates without refreshing to the homepage.
|
||||||
|
* API:
|
||||||
|
* New: Added update_check to the API.
|
||||||
|
* Fix: delete_media_info_cache not deleting the cache.
|
||||||
|
* Change: Document "refresh" parameter for get_library_media_info.
|
||||||
|
* Other:
|
||||||
|
* Fix: Show the full changelog since v2 on a fresh install.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.18-beta (2018-02-12)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Default text for Tautulli update notifications using the wrong parameter.
|
||||||
|
* Fix: Playback pause and resume notifications only triggering once.
|
||||||
|
* Change: Negative operators for custom conditions now use "and" instead of "or".
|
||||||
|
* UI:
|
||||||
|
* New: Added button to delete the 3rd party lookup info from the info pages.
|
||||||
|
* Fix: Missing host info in the login logs when logging in using Firefox.
|
||||||
|
* Change: Cleaned up settings. Advanced settings are now hidden behind a toggle.
|
||||||
|
* API:
|
||||||
|
* New: Updated API documentation for v2.
|
||||||
|
* Other:
|
||||||
|
* Fix: DeprecationWarning when using HTTPS with self-signed certificates.
|
||||||
|
* Change: Deleting the Imgur poster URL also deletes the poster from Imgur (only available for new uploads).
|
||||||
|
* Change: GitHub repository moved to Tautulli/Tautulli. Old GitHub URLs will still work.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.17-beta (2018-02-03)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Unable to use @ mentions tags for Discord and Slack.
|
||||||
|
* New: Added Zapier notification agent.
|
||||||
|
* API:
|
||||||
|
* Fix: get_synced_items returning no results.
|
||||||
|
* Fix: get_library_media_info returning incorrect media type for photo albums.
|
||||||
|
* Fix: get_library_media_info not being able to sort by title.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.16-beta (2018-01-30)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Timestamp sometimes showing as "0:60" on the activity cards.
|
||||||
|
* Fix: Incorrect session information being shown for playback of synced content.
|
||||||
|
* Fix: Sessions not being stopped when "Playback Stopped" notifications were enabled.
|
||||||
|
* UI:
|
||||||
|
* Fix: Stream resolution showing up as "unknown" on the graphs.
|
||||||
|
* New: Added user filter to the Synced Items table.
|
||||||
|
* Other:
|
||||||
|
* New: Option to use the Plex server update channel when checking for updates.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.15-beta (2018-01-27)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Live TV sessions not being stopped in History.
|
||||||
|
* Fix: Stream location showing as "unknown" on the activity cards.
|
||||||
|
* New: Improved Live TV details on the activity cards.
|
||||||
|
* Notifications:
|
||||||
|
* New: Added labels and collections to notification parameters.
|
||||||
|
* New: Added more server details to notification parameters.
|
||||||
|
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.14-beta (2018-01-20)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Change: Added "Cellular" bandwidth to "WAN" in activity header.
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Plex Web URL for tracks now go to the album page.
|
||||||
|
* Fix: Recently added notifications being sent for the entire library when DVR EPG data was refreshed.
|
||||||
|
* Fix: Notifier settings not loading with an apostrophe in the custom condition values.
|
||||||
|
* Fix: Custom email addresses not being saved when closing the notifier settings.
|
||||||
|
* Change: Re-enabled Browser notifications.
|
||||||
|
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
|
||||||
|
* Change: Emails no longer automatically insert HTML line breaks.
|
||||||
|
* Change: "Date" header added to email notifications.
|
||||||
|
* UI:
|
||||||
|
* Change: Show all changelogs since the previous version when updating.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.13-beta (2018-01-13)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* New: Added dropdown selection for email addresses of shared users.
|
||||||
|
* New: Added more notification options for Join.
|
||||||
|
* Change: Show "OR" between custom condition values.
|
||||||
|
* Other:
|
||||||
|
* New: Use JSON Web Tokens for authentication. Login now works with SSO applications.
|
||||||
|
* New: Allow the Plex server admin to login as a Tautulli admin using their Plex.tv account.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.12-beta (2018-01-07)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Incorrect Plex URL parameter value.
|
||||||
|
* Change: Custom condition logic is now optional. An implicit "and" is applied between all conditions if the logic is blank.
|
||||||
|
* UI:
|
||||||
|
* New: Added separate required LAN/WAN bandwidth in the activity header.
|
||||||
|
* API:
|
||||||
|
* Fix: Notify API command not sending notifications.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.11-beta (2018-01-05)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Some notification parameters showing up blank.
|
||||||
|
* UI:
|
||||||
|
* Fix: Stream data showing up as "None" for pre-v2 history.
|
||||||
|
* Other:
|
||||||
|
* Fix: Ability to login using the hashed password.
|
||||||
|
|
||||||
|
|
||||||
## v2.0.10-beta (2018-01-04)
|
## v2.0.10-beta (2018-01-04)
|
||||||
|
|
||||||
* Monitoring:
|
* Monitoring:
|
||||||
|
|||||||
@@ -1,48 +1,7 @@
|
|||||||
# Contributing to PlexPy
|
# Contributing to Tautulli
|
||||||
|
|
||||||
## Issues
|
|
||||||
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs. However, we are glad to help you, and make sure the problem is not caused by PlexPy, but don't expect step-by-step answers.
|
|
||||||
|
|
||||||
##### Many issues can simply be solved by:
|
|
||||||
|
|
||||||
- Making sure you update to the latest version.
|
|
||||||
- Turning your device off and on again.
|
|
||||||
- Analyzing your logs, you just might find the solution yourself!
|
|
||||||
- Using the **search** function to see if this issue has already been reported/solved.
|
|
||||||
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
|
|
||||||
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
|
|
||||||
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
|
|
||||||
- For basic questions try asking on [Gitter](https://gitter.im/plexpy/general) or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
|
|
||||||
|
|
||||||
##### If nothing has worked:
|
|
||||||
|
|
||||||
1. Open a new issue on the GitHub [issue tracker](http://github.com/JonnyWong16/plexpy/issues).
|
|
||||||
2. Provide a clear title to easily help identify your problem.
|
|
||||||
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
|
|
||||||
4. Make sure you provide the following information:
|
|
||||||
- [ ] Version
|
|
||||||
- [ ] Branch
|
|
||||||
- [ ] Commit hash
|
|
||||||
- [ ] Operating system
|
|
||||||
- [ ] Python version
|
|
||||||
- [ ] What you did?
|
|
||||||
- [ ] What happened?
|
|
||||||
- [ ] What you expected?
|
|
||||||
- [ ] How can we reproduce your issue?
|
|
||||||
- [ ] What are your (relevant) settings?
|
|
||||||
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
|
|
||||||
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
|
|
||||||
|
|
||||||
## Feature Requests
|
|
||||||
|
|
||||||
Feature requests are handled on [FeatHub](http://feathub.com/JonnyWong16/plexpy).
|
|
||||||
|
|
||||||
1. Search the existing requests to see if your suggestion has already been submitted.
|
|
||||||
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
|
|
||||||
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
|
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
If you think you can contribute code to the PlexPy repository, do not hesitate to submit a pull request.
|
If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request.
|
||||||
|
|
||||||
### Branches
|
### Branches
|
||||||
All pull requests should be based on the `dev` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/dev -b FEATURE_NAME`. Use meaningful commit messages.
|
All pull requests should be based on the `dev` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/dev -b FEATURE_NAME`. Use meaningful commit messages.
|
||||||
@@ -50,12 +9,12 @@ All pull requests should be based on the `dev` branch, to minimize cross merges.
|
|||||||
### Python Code
|
### Python Code
|
||||||
|
|
||||||
#### Compatibility
|
#### Compatibility
|
||||||
The code should work with Python 2.6 and 2.7. Note that PlexPy runs on different platforms, including Network Attached Storage devices such as Synology.
|
The code should work with Python 2.7. Note that Tautulli runs on different platforms, including Network Attached Storage devices such as Synology.
|
||||||
|
|
||||||
Re-use existing code. Do not hesitate to add logging in your code. You can the logger module `plexpy.logger.*` for this. Web requests are invoked via `plexpy.request.*` and derived ones. Use these methods to automatically add proper and meaningful error handling.
|
Re-use existing code. Do not hesitate to add logging in your code. You can the logger module `plexpy.logger.*` for this. Web requests are invoked via `plexpy.request.*` and derived ones. Use these methods to automatically add proper and meaningful error handling.
|
||||||
|
|
||||||
#### Code conventions
|
#### Code conventions
|
||||||
Although PlexPy did not adapt a code convention in the past, we try to follow the [PEP8](http://legacy.python.org/dev/peps/pep-0008/) conventions for future code. A short summary to remind you (copied from http://wiki.ros.org/PyStyleGuide):
|
Although Tautulli did not adapt a code convention in the past, we try to follow the [PEP8](http://legacy.python.org/dev/peps/pep-0008/) conventions for future code. A short summary to remind you (copied from http://wiki.ros.org/PyStyleGuide):
|
||||||
|
|
||||||
* 4 space indentation
|
* 4 space indentation
|
||||||
* 80 characters per line
|
* 80 characters per line
|
||||||
@@ -71,12 +30,12 @@ Although PlexPy did not adapt a code convention in the past, we try to follow th
|
|||||||
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
|
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
|
||||||
|
|
||||||
#### Continuous Integration
|
#### Continuous Integration
|
||||||
PlexPy has a configuration file for [travis-ci](https://travis-ci.org/). You can add your forked repo to Travis to have it check your code against PEP8, PyLint, and PyFlakes for you. Your pull request will show a green check mark or a red cross on each tested commit, depending on if linting passes.
|
Tautulli has a configuration file for [travis-ci](https://travis-ci.org/). You can add your forked repo to Travis to have it check your code against PEP8, PyLint, and PyFlakes for you. Your pull request will show a green check mark or a red cross on each tested commit, depending on if linting passes.
|
||||||
|
|
||||||
### HTML/Template code
|
### HTML/Template code
|
||||||
|
|
||||||
#### Compatibility
|
#### Compatibility
|
||||||
HTML5 compatible browsers are targetted. There is no specific mobile version of PlexPy yet.
|
HTML5 compatible browsers are targetted. There is no specific mobile version of Tautulli yet.
|
||||||
|
|
||||||
#### Conventions
|
#### Conventions
|
||||||
* 4 space indentation
|
* 4 space indentation
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Reporting Issues:
|
|||||||
Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
|
Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
|
||||||
|
|
||||||
Feature Requests:
|
Feature Requests:
|
||||||
* Feature requests are handled on FeatHub: http://feathub.com/JonnyWong16/plexpy
|
* Feature requests are handled on FeatHub: http://feathub.com/Tautulli/Tautulli
|
||||||
* Do not post them on the GitHub issues tracker.
|
* Do not post them on the GitHub issues tracker.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
242
PlexPy.py
242
PlexPy.py
@@ -21,246 +21,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
from Tautulli import main
|
||||||
import sys
|
|
||||||
|
|
||||||
# Ensure lib added to path, before any other imports
|
# Call main() from Tautulli.py
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import locale
|
|
||||||
import signal
|
|
||||||
import time
|
|
||||||
|
|
||||||
import plexpy
|
|
||||||
from plexpy import config, database, logger, web_socket, webstart
|
|
||||||
|
|
||||||
|
|
||||||
# Register signals, such as CTRL + C
|
|
||||||
signal.signal(signal.SIGINT, plexpy.sig_handler)
|
|
||||||
signal.signal(signal.SIGTERM, plexpy.sig_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""
|
|
||||||
Tautulli application entry point. Parses arguments, setups encoding and
|
|
||||||
initializes the application.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Fixed paths to Tautulli
|
|
||||||
if hasattr(sys, 'frozen'):
|
|
||||||
plexpy.FULL_PATH = os.path.abspath(sys.executable)
|
|
||||||
else:
|
|
||||||
plexpy.FULL_PATH = os.path.abspath(__file__)
|
|
||||||
|
|
||||||
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
|
|
||||||
plexpy.ARGS = sys.argv[1:]
|
|
||||||
|
|
||||||
# From sickbeard
|
|
||||||
plexpy.SYS_PLATFORM = sys.platform
|
|
||||||
plexpy.SYS_ENCODING = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
|
||||||
plexpy.SYS_ENCODING = locale.getpreferredencoding()
|
|
||||||
except (locale.Error, IOError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# for OSes that are poorly configured I'll just force UTF-8
|
|
||||||
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
|
||||||
plexpy.SYS_ENCODING = 'UTF-8'
|
|
||||||
|
|
||||||
# Set up and gather command line arguments
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='A Python based monitoring and tracking tool for Plex Media Server.')
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
|
|
||||||
parser.add_argument(
|
|
||||||
'-q', '--quiet', action='store_true', help='Turn off console logging')
|
|
||||||
parser.add_argument(
|
|
||||||
'-d', '--daemon', action='store_true', help='Run as a daemon')
|
|
||||||
parser.add_argument(
|
|
||||||
'-p', '--port', type=int, help='Force Tautulli to run on a specified port')
|
|
||||||
parser.add_argument(
|
|
||||||
'--dev', action='store_true', help='Start Tautulli in the development environment')
|
|
||||||
parser.add_argument(
|
|
||||||
'--datadir', help='Specify a directory where to store your data files')
|
|
||||||
parser.add_argument(
|
|
||||||
'--config', help='Specify a config file to use')
|
|
||||||
parser.add_argument(
|
|
||||||
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
|
|
||||||
parser.add_argument(
|
|
||||||
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--nofork', action='store_true', help='Start Tautulli as a service, do not fork when restarting')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
plexpy.VERBOSE = True
|
|
||||||
if args.quiet:
|
|
||||||
plexpy.QUIET = True
|
|
||||||
|
|
||||||
# Do an intial setup of the logger.
|
|
||||||
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
|
|
||||||
verbose=plexpy.VERBOSE)
|
|
||||||
|
|
||||||
if args.dev:
|
|
||||||
plexpy.DEV = True
|
|
||||||
logger.debug(u"Tautulli is running in the dev environment.")
|
|
||||||
|
|
||||||
if args.daemon:
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
sys.stderr.write(
|
|
||||||
"Daemonizing not supported under Windows, starting normally\n")
|
|
||||||
else:
|
|
||||||
plexpy.DAEMON = True
|
|
||||||
plexpy.QUIET = True
|
|
||||||
|
|
||||||
if args.nofork:
|
|
||||||
plexpy.NOFORK = True
|
|
||||||
logger.info("Tautulli is running as a service, it will not fork when restarted.")
|
|
||||||
|
|
||||||
if args.pidfile:
|
|
||||||
plexpy.PIDFILE = str(args.pidfile)
|
|
||||||
|
|
||||||
# If the pidfile already exists, plexpy may still be running, so
|
|
||||||
# exit
|
|
||||||
if os.path.exists(plexpy.PIDFILE):
|
|
||||||
try:
|
|
||||||
with open(plexpy.PIDFILE, 'r') as fp:
|
|
||||||
pid = int(fp.read())
|
|
||||||
os.kill(pid, 0)
|
|
||||||
except IOError as e:
|
|
||||||
raise SystemExit("Unable to read PID file: %s", e)
|
|
||||||
except OSError:
|
|
||||||
logger.warn("PID file '%s' already exists, but PID %d is " \
|
|
||||||
"not running. Ignoring PID file." %
|
|
||||||
(plexpy.PIDFILE, pid))
|
|
||||||
else:
|
|
||||||
# The pidfile exists and points to a live PID. plexpy may
|
|
||||||
# still be running, so exit.
|
|
||||||
raise SystemExit("PID file '%s' already exists. Exiting." %
|
|
||||||
plexpy.PIDFILE)
|
|
||||||
|
|
||||||
# The pidfile is only useful in daemon mode, make sure we can write the
|
|
||||||
# file properly
|
|
||||||
if plexpy.DAEMON:
|
|
||||||
plexpy.CREATEPID = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(plexpy.PIDFILE, 'w') as fp:
|
|
||||||
fp.write("pid\n")
|
|
||||||
except IOError as e:
|
|
||||||
raise SystemExit("Unable to write PID file: %s", e)
|
|
||||||
else:
|
|
||||||
logger.warn("Not running in daemon mode. PID file creation " \
|
|
||||||
"disabled.")
|
|
||||||
|
|
||||||
# Determine which data directory and config file to use
|
|
||||||
if args.datadir:
|
|
||||||
plexpy.DATA_DIR = args.datadir
|
|
||||||
else:
|
|
||||||
plexpy.DATA_DIR = plexpy.PROG_DIR
|
|
||||||
|
|
||||||
if args.config:
|
|
||||||
config_file = args.config
|
|
||||||
else:
|
|
||||||
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
|
|
||||||
|
|
||||||
# Try to create the DATA_DIR if it doesn't exist
|
|
||||||
if not os.path.exists(plexpy.DATA_DIR):
|
|
||||||
try:
|
|
||||||
os.makedirs(plexpy.DATA_DIR)
|
|
||||||
except OSError:
|
|
||||||
raise SystemExit(
|
|
||||||
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
|
|
||||||
|
|
||||||
# Make sure the DATA_DIR is writeable
|
|
||||||
if not os.access(plexpy.DATA_DIR, os.W_OK):
|
|
||||||
raise SystemExit(
|
|
||||||
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
|
|
||||||
|
|
||||||
# Put the database in the DATA_DIR
|
|
||||||
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
|
|
||||||
|
|
||||||
if plexpy.DAEMON:
|
|
||||||
plexpy.daemonize()
|
|
||||||
|
|
||||||
# Read config and start logging
|
|
||||||
plexpy.initialize(config_file)
|
|
||||||
|
|
||||||
# Start the background threads
|
|
||||||
plexpy.start()
|
|
||||||
|
|
||||||
# Open connection for websocket
|
|
||||||
try:
|
|
||||||
web_socket.start_thread()
|
|
||||||
except:
|
|
||||||
logger.warn(u"Websocket :: Unable to open connection.")
|
|
||||||
plexpy.initialize_scheduler()
|
|
||||||
|
|
||||||
# Force the http port if neccessary
|
|
||||||
if args.port:
|
|
||||||
http_port = args.port
|
|
||||||
logger.info('Using forced web server port: %i', http_port)
|
|
||||||
else:
|
|
||||||
http_port = int(plexpy.CONFIG.HTTP_PORT)
|
|
||||||
|
|
||||||
# Check if pyOpenSSL is installed. It is required for certificate generation
|
|
||||||
# and for CherryPy.
|
|
||||||
if plexpy.CONFIG.ENABLE_HTTPS:
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
except ImportError:
|
|
||||||
logger.warn("The pyOpenSSL module is missing. Install this " \
|
|
||||||
"module to enable HTTPS. HTTPS will be disabled.")
|
|
||||||
plexpy.CONFIG.ENABLE_HTTPS = False
|
|
||||||
|
|
||||||
# Try to start the server. Will exit here is address is already in use.
|
|
||||||
web_config = {
|
|
||||||
'http_port': http_port,
|
|
||||||
'http_host': plexpy.CONFIG.HTTP_HOST,
|
|
||||||
'http_root': plexpy.CONFIG.HTTP_ROOT,
|
|
||||||
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
|
|
||||||
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
|
|
||||||
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
|
|
||||||
'https_cert': plexpy.CONFIG.HTTPS_CERT,
|
|
||||||
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
|
|
||||||
'https_key': plexpy.CONFIG.HTTPS_KEY,
|
|
||||||
'http_username': plexpy.CONFIG.HTTP_USERNAME,
|
|
||||||
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
|
|
||||||
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
|
|
||||||
}
|
|
||||||
webstart.initialize(web_config)
|
|
||||||
|
|
||||||
# Open webbrowser
|
|
||||||
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
|
|
||||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
|
|
||||||
plexpy.CONFIG.HTTP_ROOT)
|
|
||||||
|
|
||||||
# Wait endlessy for a signal to happen
|
|
||||||
while True:
|
|
||||||
if not plexpy.SIGNAL:
|
|
||||||
try:
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
plexpy.SIGNAL = 'shutdown'
|
|
||||||
else:
|
|
||||||
logger.info('Received signal: %s', plexpy.SIGNAL)
|
|
||||||
|
|
||||||
if plexpy.SIGNAL == 'shutdown':
|
|
||||||
plexpy.shutdown()
|
|
||||||
elif plexpy.SIGNAL == 'restart':
|
|
||||||
plexpy.shutdown(restart=True)
|
|
||||||
elif plexpy.SIGNAL == 'checkout':
|
|
||||||
plexpy.shutdown(restart=True, checkout=True)
|
|
||||||
else:
|
|
||||||
plexpy.shutdown(restart=True, update=True)
|
|
||||||
|
|
||||||
plexpy.SIGNAL = None
|
|
||||||
|
|
||||||
# Call main()
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -1,8 +1,8 @@
|
|||||||
# Tautulli
|
# Tautulli
|
||||||
|
|
||||||
[](https://discord.gg/36ggawe)
|
[](https://discord.gg/tQcWEUp)
|
||||||
[](https://www.reddit.com/r/Tautulli/)
|
[](https://www.reddit.com/r/Tautulli/)
|
||||||
[](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program)
|
[](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server)
|
||||||
|
|
||||||
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
|
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
|
||||||
|
|
||||||
@@ -27,56 +27,19 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
|||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
* [Full preview gallery on our website](http://tautulli.com)
|
* [Full preview gallery available on our website](http://tautulli.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation and Support
|
## Installation and Support
|
||||||
|
|
||||||
* [Installation Guides](https://github.com/JonnyWong16/plexpy/wiki/Installation) shows you how to install Tautulli.
|
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
|
||||||
* [FAQs](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)) in the wiki can help you with common problems.
|
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
|
||||||
|
* Support is available on [Discord](https://discord.gg/tQcWEUp), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server).
|
||||||
|
|
||||||
**Support** the project by implementing new features, solving support tickets and provide bug fixes.
|
## Issues & Feature Requests
|
||||||
|
|
||||||
## Issues
|
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
|
||||||
|
|
||||||
##### Many issues can simply be solved by:
|
|
||||||
|
|
||||||
- Making sure you update to the latest version.
|
|
||||||
- Turning your device off and on again.
|
|
||||||
- Analyzing your logs, you just might find the solution yourself!
|
|
||||||
- Using the **search** function to see if this issue has already been reported/solved.
|
|
||||||
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
|
|
||||||
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
|
|
||||||
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
|
|
||||||
- For basic questions try asking on [Discord](https://discord.gg/36ggawe), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
|
|
||||||
|
|
||||||
##### If nothing has worked:
|
|
||||||
|
|
||||||
1. Open a new issue on the GitHub [issue tracker](http://github.com/JonnyWong16/plexpy/issues).
|
|
||||||
2. Provide a clear title to easily help identify your problem.
|
|
||||||
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
|
|
||||||
4. Make sure you provide the following information:
|
|
||||||
- [ ] Version
|
|
||||||
- [ ] Branch
|
|
||||||
- [ ] Commit hash
|
|
||||||
- [ ] Operating system
|
|
||||||
- [ ] Python version
|
|
||||||
- [ ] What you did?
|
|
||||||
- [ ] What happened?
|
|
||||||
- [ ] What you expected?
|
|
||||||
- [ ] How can we reproduce your issue?
|
|
||||||
- [ ] What are your (relevant) settings?
|
|
||||||
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
|
|
||||||
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
|
|
||||||
|
|
||||||
## Feature Requests
|
|
||||||
|
|
||||||
Feature requests are handled on [FeatHub](http://feathub.com/JonnyWong16/plexpy).
|
|
||||||
|
|
||||||
1. Search the existing requests to see if your suggestion has already been submitted.
|
|
||||||
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
|
|
||||||
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
267
Tautulli.py
Executable file
267
Tautulli.py
Executable file
@@ -0,0 +1,267 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
|
||||||
|
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
|
||||||
|
''''which python2.7 >/dev/null 2>&1 && exec python2.7 "$0" "$@" # '''
|
||||||
|
''''exec echo "Error: Python not found!" # '''
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
|
#
|
||||||
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Tautulli is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure lib added to path, before any other imports
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import locale
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
|
||||||
|
import plexpy
|
||||||
|
from plexpy import config, database, logger, webstart
|
||||||
|
|
||||||
|
|
||||||
|
# Register signals, such as CTRL + C
|
||||||
|
signal.signal(signal.SIGINT, plexpy.sig_handler)
|
||||||
|
signal.signal(signal.SIGTERM, plexpy.sig_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Tautulli application entry point. Parses arguments, setups encoding and
|
||||||
|
initializes the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Fixed paths to Tautulli
|
||||||
|
if hasattr(sys, 'frozen'):
|
||||||
|
plexpy.FULL_PATH = os.path.abspath(sys.executable)
|
||||||
|
else:
|
||||||
|
plexpy.FULL_PATH = os.path.abspath(__file__)
|
||||||
|
|
||||||
|
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
|
||||||
|
plexpy.ARGS = sys.argv[1:]
|
||||||
|
|
||||||
|
# From sickbeard
|
||||||
|
plexpy.SYS_PLATFORM = sys.platform
|
||||||
|
plexpy.SYS_ENCODING = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
|
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
|
||||||
|
except (locale.Error, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# for OSes that are poorly configured I'll just force UTF-8
|
||||||
|
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||||
|
plexpy.SYS_ENCODING = 'UTF-8'
|
||||||
|
|
||||||
|
# Set up and gather command line arguments
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='A Python based monitoring and tracking tool for Plex Media Server.')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
|
||||||
|
parser.add_argument(
|
||||||
|
'-q', '--quiet', action='store_true', help='Turn off console logging')
|
||||||
|
parser.add_argument(
|
||||||
|
'-d', '--daemon', action='store_true', help='Run as a daemon')
|
||||||
|
parser.add_argument(
|
||||||
|
'-p', '--port', type=int, help='Force Tautulli to run on a specified port')
|
||||||
|
parser.add_argument(
|
||||||
|
'--dev', action='store_true', help='Start Tautulli in the development environment')
|
||||||
|
parser.add_argument(
|
||||||
|
'--datadir', help='Specify a directory where to store your data files')
|
||||||
|
parser.add_argument(
|
||||||
|
'--config', help='Specify a config file to use')
|
||||||
|
parser.add_argument(
|
||||||
|
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
|
||||||
|
parser.add_argument(
|
||||||
|
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--nofork', action='store_true', help='Start Tautulli as a service, do not fork when restarting')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
plexpy.VERBOSE = True
|
||||||
|
if args.quiet:
|
||||||
|
plexpy.QUIET = True
|
||||||
|
|
||||||
|
# Do an intial setup of the logger.
|
||||||
|
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
|
||||||
|
verbose=plexpy.VERBOSE)
|
||||||
|
|
||||||
|
if args.dev:
|
||||||
|
plexpy.DEV = True
|
||||||
|
logger.debug(u"Tautulli is running in the dev environment.")
|
||||||
|
|
||||||
|
if args.daemon:
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
sys.stderr.write(
|
||||||
|
"Daemonizing not supported under Windows, starting normally\n")
|
||||||
|
else:
|
||||||
|
plexpy.DAEMON = True
|
||||||
|
plexpy.QUIET = True
|
||||||
|
|
||||||
|
if args.nofork:
|
||||||
|
plexpy.NOFORK = True
|
||||||
|
logger.info("Tautulli is running as a service, it will not fork when restarted.")
|
||||||
|
|
||||||
|
if args.pidfile:
|
||||||
|
plexpy.PIDFILE = str(args.pidfile)
|
||||||
|
|
||||||
|
# If the pidfile already exists, plexpy may still be running, so
|
||||||
|
# exit
|
||||||
|
if os.path.exists(plexpy.PIDFILE):
|
||||||
|
try:
|
||||||
|
with open(plexpy.PIDFILE, 'r') as fp:
|
||||||
|
pid = int(fp.read())
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except IOError as e:
|
||||||
|
raise SystemExit("Unable to read PID file: %s", e)
|
||||||
|
except OSError:
|
||||||
|
logger.warn("PID file '%s' already exists, but PID %d is " \
|
||||||
|
"not running. Ignoring PID file." %
|
||||||
|
(plexpy.PIDFILE, pid))
|
||||||
|
else:
|
||||||
|
# The pidfile exists and points to a live PID. plexpy may
|
||||||
|
# still be running, so exit.
|
||||||
|
raise SystemExit("PID file '%s' already exists. Exiting." %
|
||||||
|
plexpy.PIDFILE)
|
||||||
|
|
||||||
|
# The pidfile is only useful in daemon mode, make sure we can write the
|
||||||
|
# file properly
|
||||||
|
if plexpy.DAEMON:
|
||||||
|
plexpy.CREATEPID = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(plexpy.PIDFILE, 'w') as fp:
|
||||||
|
fp.write("pid\n")
|
||||||
|
except IOError as e:
|
||||||
|
raise SystemExit("Unable to write PID file: %s", e)
|
||||||
|
else:
|
||||||
|
logger.warn("Not running in daemon mode. PID file creation " \
|
||||||
|
"disabled.")
|
||||||
|
|
||||||
|
# Determine which data directory and config file to use
|
||||||
|
if args.datadir:
|
||||||
|
plexpy.DATA_DIR = args.datadir
|
||||||
|
else:
|
||||||
|
plexpy.DATA_DIR = plexpy.PROG_DIR
|
||||||
|
|
||||||
|
if args.config:
|
||||||
|
config_file = args.config
|
||||||
|
else:
|
||||||
|
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
|
||||||
|
|
||||||
|
# Try to create the DATA_DIR if it doesn't exist
|
||||||
|
if not os.path.exists(plexpy.DATA_DIR):
|
||||||
|
try:
|
||||||
|
os.makedirs(plexpy.DATA_DIR)
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit(
|
||||||
|
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
|
||||||
|
|
||||||
|
# Make sure the DATA_DIR is writeable
|
||||||
|
if not os.access(plexpy.DATA_DIR, os.W_OK):
|
||||||
|
raise SystemExit(
|
||||||
|
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
|
||||||
|
|
||||||
|
# Put the database in the DATA_DIR
|
||||||
|
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
|
||||||
|
|
||||||
|
# Move 'plexpy.db' to 'tautulli.db'
|
||||||
|
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
|
||||||
|
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
|
||||||
|
try:
|
||||||
|
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
|
||||||
|
except OSError as e:
|
||||||
|
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
|
||||||
|
|
||||||
|
if plexpy.DAEMON:
|
||||||
|
plexpy.daemonize()
|
||||||
|
|
||||||
|
# Read config and start logging
|
||||||
|
plexpy.initialize(config_file)
|
||||||
|
|
||||||
|
# Start the background threads
|
||||||
|
plexpy.start()
|
||||||
|
|
||||||
|
# Force the http port if neccessary
|
||||||
|
if args.port:
|
||||||
|
http_port = args.port
|
||||||
|
logger.info('Using forced web server port: %i', http_port)
|
||||||
|
else:
|
||||||
|
http_port = int(plexpy.CONFIG.HTTP_PORT)
|
||||||
|
|
||||||
|
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||||
|
# and for CherryPy.
|
||||||
|
if plexpy.CONFIG.ENABLE_HTTPS:
|
||||||
|
try:
|
||||||
|
import OpenSSL
|
||||||
|
except ImportError:
|
||||||
|
logger.warn("The pyOpenSSL module is missing. Install this " \
|
||||||
|
"module to enable HTTPS. HTTPS will be disabled.")
|
||||||
|
plexpy.CONFIG.ENABLE_HTTPS = False
|
||||||
|
|
||||||
|
# Try to start the server. Will exit here is address is already in use.
|
||||||
|
web_config = {
|
||||||
|
'http_port': http_port,
|
||||||
|
'http_host': plexpy.CONFIG.HTTP_HOST,
|
||||||
|
'http_root': plexpy.CONFIG.HTTP_ROOT,
|
||||||
|
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
|
||||||
|
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
|
||||||
|
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
|
||||||
|
'https_cert': plexpy.CONFIG.HTTPS_CERT,
|
||||||
|
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
|
||||||
|
'https_key': plexpy.CONFIG.HTTPS_KEY,
|
||||||
|
'http_username': plexpy.CONFIG.HTTP_USERNAME,
|
||||||
|
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
|
||||||
|
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
|
||||||
|
}
|
||||||
|
webstart.initialize(web_config)
|
||||||
|
|
||||||
|
# Open webbrowser
|
||||||
|
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
|
||||||
|
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
|
||||||
|
plexpy.CONFIG.HTTP_ROOT)
|
||||||
|
|
||||||
|
# Wait endlessy for a signal to happen
|
||||||
|
while True:
|
||||||
|
if not plexpy.SIGNAL:
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
plexpy.SIGNAL = 'shutdown'
|
||||||
|
else:
|
||||||
|
logger.info('Received signal: %s', plexpy.SIGNAL)
|
||||||
|
|
||||||
|
if plexpy.SIGNAL == 'shutdown':
|
||||||
|
plexpy.shutdown()
|
||||||
|
elif plexpy.SIGNAL == 'restart':
|
||||||
|
plexpy.shutdown(restart=True)
|
||||||
|
elif plexpy.SIGNAL == 'checkout':
|
||||||
|
plexpy.shutdown(restart=True, checkout=True)
|
||||||
|
else:
|
||||||
|
plexpy.shutdown(restart=True, update=True)
|
||||||
|
|
||||||
|
plexpy.SIGNAL = None
|
||||||
|
|
||||||
|
# Call main()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import version
|
from plexpy import version
|
||||||
from plexpy.helpers import anon_url
|
from plexpy.helpers import anon_url
|
||||||
|
from plexpy.notifiers import BROWSER_NOTIFIERS
|
||||||
%>
|
%>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
||||||
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
|
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
||||||
${next.headIncludes()}
|
${next.headIncludes()}
|
||||||
@@ -43,16 +44,24 @@
|
|||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
||||||
<div id="updatebar" style="display: none;">
|
<div id="updatebar" style="display: none;">
|
||||||
You're running an unknown version of Tautulli.<br />
|
You are running an unknown version of Tautulli.<br />
|
||||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
|
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
</div>
|
</div>
|
||||||
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
|
||||||
<div id="updatebar" style="display: none;">
|
<div id="updatebar" style="display: none;">
|
||||||
A <a href="${anon_url('https://github.com/%s/plexpy/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
|
||||||
newer version</a> is available.<br />
|
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
|
||||||
You're ${plexpy.COMMITS_BEHIND} commits behind.<br />
|
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.INSTALL_TYPE != 'win':
|
||||||
|
<div id="updatebar" style="display: none;">
|
||||||
|
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
||||||
|
newer version</a> of Tautulli is available!<br />
|
||||||
|
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
|
||||||
|
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div id="updatebar" style="display: none;"></div>
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
<nav class="navbar navbar-fixed-top">
|
<nav class="navbar navbar-fixed-top">
|
||||||
@@ -65,7 +74,7 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="home" title="Tautulli">
|
<a class="navbar-brand" href="home" title="Tautulli">
|
||||||
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object>
|
<img src="${http_root}images/logo-tautulli-45.png" height="45" alt="PlexPy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
|
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
|
||||||
@@ -124,7 +133,7 @@
|
|||||||
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
|
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
|
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
|
||||||
<li><a href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
|
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
|
||||||
<li><a href="settings?support=true"><i class="fa fa-fw fa-comment"></i> Support</a></li>
|
<li><a href="settings?support=true"><i class="fa fa-fw fa-comment"></i> Support</a></li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
|
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
|
||||||
@@ -138,7 +147,7 @@
|
|||||||
<li><a href="#" data-target="#admin-login-modal" data-toggle="modal"><i class="fa fa-fw fa-lock"></i> Admin Login</a></li>
|
<li><a href="#" data-target="#admin-login-modal" data-toggle="modal"><i class="fa fa-fw fa-lock"></i> Admin Login</a></li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
% endif
|
% endif
|
||||||
% if _session['expiry']:
|
% if _session['exp']:
|
||||||
<li><a href="${http_root}auth/logout"><i class="fa fa-fw fa-sign-out"></i> Sign Out</a></li>
|
<li><a href="${http_root}auth/logout"><i class="fa fa-fw fa-sign-out"></i> Sign Out</a></li>
|
||||||
% endif
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
@@ -161,7 +170,7 @@ ${next.modalIncludes()}
|
|||||||
<div id="admin-login-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="admin-login-modal">
|
<div id="admin-login-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="admin-login-modal">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form action="${http_root}auth/login" method="post">
|
<form id="login-form">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
<h4 class="modal-title">Admin Login</h4>
|
<h4 class="modal-title">Admin Login</h4>
|
||||||
@@ -190,7 +199,8 @@ ${next.modalIncludes()}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
<span id="incorrect-login" style="padding-right: 25px; display: none;">Incorrect username or password.</span>
|
||||||
|
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
||||||
</form>
|
</form>
|
||||||
@@ -217,15 +227,23 @@ ${next.modalIncludes()}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
||||||
<li class="active"><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||||
<li><a href="#flattr-donation" role="tab" data-toggle="tab">Flattr</a></li>
|
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
|
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
|
||||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
|
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
|
||||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
|
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
|
||||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
|
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="paypal-donation" style="text-align: center">
|
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
|
||||||
|
<p>
|
||||||
|
Click the button below to continue to Patreon.
|
||||||
|
</p>
|
||||||
|
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank">
|
||||||
|
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
|
||||||
<p>
|
<p>
|
||||||
Click the button below to continue to PayPal.
|
Click the button below to continue to PayPal.
|
||||||
</p>
|
</p>
|
||||||
@@ -233,14 +251,6 @@ ${next.modalIncludes()}
|
|||||||
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="flattr-donation" style="text-align: center">
|
|
||||||
<p>
|
|
||||||
Click the button below to continue to Flattr.
|
|
||||||
</p>
|
|
||||||
<a href="${anon_url('https://flattr.com/submit/auto?user_id=JonnyWong16&url=https://github.com/JonnyWong16/plexpy&title=Tautulli&language=en_GB&tags=github&category=software')}" target="_blank">
|
|
||||||
<img src="images/flattr-badge-large.png" alt="Flattr">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div role="tabpanel" class="tab-pane" id="crypto-donation">
|
<div role="tabpanel" class="tab-pane" id="crypto-donation">
|
||||||
<label>QR Code</label>
|
<label>QR Code</label>
|
||||||
<pre id="crypto_qr_code" style="text-align: center"></pre>
|
<pre id="crypto_qr_code" style="text-align: center"></pre>
|
||||||
@@ -282,16 +292,53 @@ ${next.modalIncludes()}
|
|||||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||||
|
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||||
|
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||||
|
% endif
|
||||||
<script>
|
<script>
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('#updateDismiss').click(function() {
|
$('body').on('click', '#updateDismiss', function() {
|
||||||
$('#updatebar').slideUp('slow');
|
$('#updatebar').fadeOut();
|
||||||
// Set cookie to remember dismiss decision for 1 hour.
|
// Set cookie to remember dismiss decision for 1 hour.
|
||||||
setCookie('updateDismiss', 'true', 1/24);
|
setCookie('updateDismiss', 'true', 1/24);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!getCookie('updateDismiss')) {
|
if (!getCookie('updateDismiss')) {
|
||||||
$('#updatebar').show();
|
if ($('#updatebar').html().length > 0) {
|
||||||
|
$('#updatebar').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUpdate(_callback) {
|
||||||
|
// Allow the update bar to show again if previously dismissed.
|
||||||
|
setCookie('updateDismiss', 'true', 0);
|
||||||
|
$.ajax({
|
||||||
|
url: 'update_check',
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
var result = $.parseJSON(xhr.responseText);
|
||||||
|
var msg = '';
|
||||||
|
if (result.update === null) {
|
||||||
|
msg = 'You are running an unknown version of Tautulli.<br />' +
|
||||||
|
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
|
$('#updatebar').html(msg).fadeIn();
|
||||||
|
} else if (result.update === true && result.release === true) {
|
||||||
|
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />' +
|
||||||
|
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
|
$('#updatebar').html(msg).fadeIn();
|
||||||
|
} else if (result.update === true && result.release === false) {
|
||||||
|
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
|
||||||
|
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />' +
|
||||||
|
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
|
$('#updatebar').html(msg).fadeIn();
|
||||||
|
} else if (result.update === false) {
|
||||||
|
showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_callback) {
|
||||||
|
_callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#nav-shutdown").click(function() {
|
$("#nav-shutdown").click(function() {
|
||||||
@@ -310,11 +357,9 @@ ${next.modalIncludes()}
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#nav-update").first().one("click", function () {
|
$('#nav-update').click(function () {
|
||||||
// Allow the update bar to show again if previously dismissed.
|
$(this).html('<i class="fa fa-fw fa-spin fa-refresh"></i> Checking');
|
||||||
setCookie('updateDismiss', 'true', 0);
|
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-circle-up"></i> Check for Updates'); });
|
||||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking');
|
|
||||||
window.location.href = "checkGithub";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
||||||
@@ -386,6 +431,29 @@ ${next.modalIncludes()}
|
|||||||
$('#admin-login-modal').on('shown.bs.modal', function () {
|
$('#admin-login-modal').on('shown.bs.modal', function () {
|
||||||
$('#admin-login-modal #username').focus()
|
$('#admin-login-modal #username').focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('#login-form').submit(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i> Sign In');
|
||||||
|
$.ajax({
|
||||||
|
url: '${http_root}auth/signin',
|
||||||
|
type: 'POST',
|
||||||
|
data: $(this).serialize(),
|
||||||
|
dataType: 'json',
|
||||||
|
statusCode: {
|
||||||
|
200: function() {
|
||||||
|
window.location = "${http_root}";
|
||||||
|
},
|
||||||
|
401: function() {
|
||||||
|
$('#incorrect-login').show();
|
||||||
|
$('#username').focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i> Sign In');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
% endif
|
% endif
|
||||||
</script>
|
</script>
|
||||||
${next.javascriptIncludes()}
|
${next.javascriptIncludes()}
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ DOCUMENTATION :: END
|
|||||||
% if plexpy.CURRENT_VERSION:
|
% if plexpy.CURRENT_VERSION:
|
||||||
<tr>
|
<tr>
|
||||||
<td>Git Branch:</td>
|
<td>Git Branch:</td>
|
||||||
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CONFIG.GIT_BRANCH}</a></td>
|
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CONFIG.GIT_BRANCH}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Git Commit Hash:</td>
|
<td>Git Commit Hash:</td>
|
||||||
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CURRENT_VERSION}</a></td>
|
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}">${plexpy.CURRENT_VERSION}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
@@ -75,18 +75,18 @@ DOCUMENTATION :: END
|
|||||||
<td class="top-line">Resources:</td>
|
<td class="top-line">Resources:</td>
|
||||||
<td class="top-line">
|
<td class="top-line">
|
||||||
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> |
|
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> |
|
||||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Source</a> |
|
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Source</a> |
|
||||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/plexpy/issues' % plexpy.CONFIG.GIT_USER)}" data-id="issue">GitHub Issues</a> |
|
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="issue">GitHub Issues</a> |
|
||||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Wiki & FAQ</a> |
|
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Wiki</a> |
|
||||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" data-id="feature request">FeatHub Feature Requests</a> |
|
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="feature request">FeatHub Feature Requests</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Support:</td>
|
<td>Support:</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/36ggawe')}" target="_blank">Tautulli Discord Server</a> |
|
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/tQcWEUp')}" target="_blank">Tautulli Discord Server</a> |
|
||||||
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
|
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
|
||||||
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program')}" target="_blank">Plex Forums</a>
|
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server')}" target="_blank">Plex Forums</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -359,11 +359,13 @@ table.display tr.shown + tr:hover {
|
|||||||
}
|
}
|
||||||
table.display tr.shown + tr:hover a,
|
table.display tr.shown + tr:hover a,
|
||||||
table.display tr.shown + tr td:hover a,
|
table.display tr.shown + tr td:hover a,
|
||||||
|
table.display tr.shown + tr td:hover a .fa,
|
||||||
table.display tr.shown + tr .pagination > .active > a,
|
table.display tr.shown + tr .pagination > .active > a,
|
||||||
table.display tr.shown + tr .pagination > .active > a:hover {
|
table.display tr.shown + tr .pagination > .active > a:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
table.display tr.shown + tr table[id^='history_child'] td:hover a,
|
table.display tr.shown + tr table[id^='history_child'] td:hover a,
|
||||||
|
table.display tr.shown + tr table[id^='history_child'] td:hover a .fa,
|
||||||
table.display tr.shown + tr table[id^='media_info_child'] > tr > td:hover a,
|
table.display tr.shown + tr table[id^='media_info_child'] > tr > td:hover a,
|
||||||
table.display tr.shown + tr table[id^='media_info_child'] tr.shown + tr table[id^='media_info_child'] td:hover a {
|
table.display tr.shown + tr table[id^='media_info_child'] tr.shown + tr table[id^='media_info_child'] td:hover a {
|
||||||
color: #cc7b19;
|
color: #cc7b19;
|
||||||
@@ -60,22 +60,57 @@ select[multiple] option {
|
|||||||
-moz-border-radius: 2px;
|
-moz-border-radius: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
select.form-control {
|
select.form-control,
|
||||||
|
div.form-control .selectize-input {
|
||||||
margin: 5px 0 5px 0;
|
margin: 5px 0 5px 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: 0px solid #444;
|
border: 0px solid #444;
|
||||||
background: #555;
|
background: #555;
|
||||||
height: 32px;
|
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
.react-selectize.root-node .react-selectize-control {
|
.react-selectize.root-node .react-selectize-control,
|
||||||
|
.selectize-control.form-control .selectize-input {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
border: 0px solid #444 !important;
|
border: 0px solid #444 !important;
|
||||||
background: #555 !important;
|
background: #555 !important;
|
||||||
padding: 1px 2px;
|
padding: 1px 2px;
|
||||||
|
transition: background-color .3s;
|
||||||
|
}
|
||||||
|
.selectize-control.form-control .selectize-input {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
.selectize-control.form-control.selectize-pms-ip .selectize-input {
|
||||||
|
padding-left: 12px !important;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
min-height: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
}
|
||||||
|
.input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
|
||||||
|
max-width: 450px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
|
||||||
|
max-width: 360px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#selectize-pms-ip-container .selectize-dropdown.form-control.selectize-pms-ip {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-dropdown .selectize-dropdown-content {
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
.wizard-input-section .selectize-dropdown.form-control.selectize-pms-ip {
|
||||||
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
@@ -83,20 +118,96 @@ select.form-control {
|
|||||||
.react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
|
.react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
|
||||||
fill: #fff !important;
|
fill: #fff !important;
|
||||||
}
|
}
|
||||||
|
.react-selectize.root-node .simple-value,
|
||||||
|
.selectize-control.multi .selectize-input > div {
|
||||||
|
background: #444444 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
padding-bottom: 2px !important;
|
||||||
|
transition: background-color .3s;
|
||||||
|
}
|
||||||
.react-selectize.root-node .simple-value span {
|
.react-selectize.root-node .simple-value span {
|
||||||
padding-bottom: 2px !important;
|
padding-bottom: 2px !important;
|
||||||
}
|
}
|
||||||
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input{
|
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .value-wrapper:not(:first-child):before {
|
||||||
|
content: "or";
|
||||||
|
padding: 0 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values.negative-operator .value-wrapper:not(:first-child):before {
|
||||||
|
content: "and" !important;
|
||||||
|
}
|
||||||
|
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input {
|
||||||
padding-top: 3px !important;
|
padding-top: 3px !important;
|
||||||
padding-bottom: 3px !important;
|
padding-bottom: 3px !important;
|
||||||
}
|
}
|
||||||
select.form-control:focus {
|
select.form-control:focus,
|
||||||
|
.react-selectize.root-node.open .react-selectize-control,
|
||||||
|
.selectize-control.form-control .selectize-input.focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
outline: thin dotted \9;
|
outline: thin dotted \9;
|
||||||
color: #555;
|
color: #555 !important;
|
||||||
background-color: #fff;
|
background-color: #fff !important;
|
||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
|
.react-selectize.root-node.open .simple-value,
|
||||||
|
.selectize-control.multi .selectize-input.focus > div,
|
||||||
|
.selectize-control.multi .selectize-input > div.active {
|
||||||
|
background: #efefef !important;
|
||||||
|
color: #333333 !important;
|
||||||
|
transition: background-color .3s;
|
||||||
|
}
|
||||||
|
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
||||||
|
fill: #999 !important;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-value {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-text + .item-value {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-value:before {
|
||||||
|
content: '<';
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-value:after {
|
||||||
|
content: '>';
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .caption {
|
||||||
|
font-size: 12px;
|
||||||
|
display: block;
|
||||||
|
color: #a0a0a0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .select-all,
|
||||||
|
.selectize-dropdown .remove-all {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .border-all {
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
margin: 9px -12px 9px -12px;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .border-all:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup-header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
select.form-control option {
|
select.form-control option {
|
||||||
color: #555;
|
color: #555;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@@ -145,7 +256,7 @@ object {
|
|||||||
}
|
}
|
||||||
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
|
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
|
||||||
background-color: #2f2f2f;
|
background-color: #2f2f2f;
|
||||||
border-color: none;
|
border-color: unset;
|
||||||
}
|
}
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: #282828;
|
background-color: #282828;
|
||||||
@@ -541,18 +652,8 @@ textarea.form-control:focus {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.form-control-feedback {
|
.form-control-feedback {
|
||||||
position: absolute;
|
|
||||||
color: #F9AA03;
|
color: #F9AA03;
|
||||||
top: 0;
|
margin: 5px 40px 5px 0;
|
||||||
right: 0;
|
|
||||||
margin: 5px 10px 5px 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: block;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
text-align: center;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
.form-control[readonly] {
|
.form-control[readonly] {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
@@ -631,8 +732,8 @@ a .users-poster-face:hover {
|
|||||||
height: 290px;
|
height: 290px;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin-right: 20px;
|
margin-right: 25px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
.dashboard-activity-container {
|
.dashboard-activity-container {
|
||||||
height: 240px;
|
height: 240px;
|
||||||
@@ -913,7 +1014,6 @@ a .users-poster-face:hover {
|
|||||||
background-image: -o-linear-gradient(top, #fbb450, #f89406);
|
background-image: -o-linear-gradient(top, #fbb450, #f89406);
|
||||||
background-image: linear-gradient(to bottom, #fbb450, #f89406);
|
background-image: linear-gradient(to bottom, #fbb450, #f89406);
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -1050,8 +1150,8 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
|||||||
height: 160px;
|
height: 160px;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin-right: 20px;
|
margin-right: 25px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
.dashboard-stats-container {
|
.dashboard-stats-container {
|
||||||
height: 160px;
|
height: 160px;
|
||||||
@@ -1319,7 +1419,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
|||||||
}
|
}
|
||||||
.dashboard-stats-info-item .sub-count {
|
.dashboard-stats-info-item .sub-count {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: 10px;
|
margin-left: 5px;
|
||||||
color: #f9be03;
|
color: #f9be03;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -1330,7 +1430,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
|||||||
}
|
}
|
||||||
.dashboard-stats-info-item .sub-divider {
|
.dashboard-stats-info-item .sub-divider {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: 10px;
|
margin-left: 5px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -1668,7 +1768,6 @@ a:hover .dashboard-recent-media-cover {
|
|||||||
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||||
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3000000', endColorstr='#e6000000', GradientType=0);
|
|
||||||
webkit-box-shadow: inset 0 0 0 2px #e9a049;
|
webkit-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
box-shadow: inset 0 0 0 2px #e9a049;
|
box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
@@ -1686,6 +1785,18 @@ a:hover .dashboard-recent-media-cover {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .3s;
|
transition: opacity .3s;
|
||||||
}
|
}
|
||||||
|
.summary-poster-face-overlay span:before {
|
||||||
|
content: "View On";
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 34px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
a:hover .summary-poster-face .summary-poster-face-overlay,
|
a:hover .summary-poster-face .summary-poster-face-overlay,
|
||||||
a:hover .summary-poster-face-episode .summary-poster-face-overlay,
|
a:hover .summary-poster-face-episode .summary-poster-face-overlay,
|
||||||
a:hover .summary-poster-face-track .summary-poster-face-overlay,
|
a:hover .summary-poster-face-track .summary-poster-face-overlay,
|
||||||
@@ -2049,6 +2160,20 @@ a:hover .item-children-poster {
|
|||||||
top: 5px;
|
top: 5px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
}
|
}
|
||||||
|
#menu_link_show_advanced_settings.active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #cc7b19;
|
||||||
|
}
|
||||||
|
.advanced-setting {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
div.advanced-setting {
|
||||||
|
border-left: 1px solid #cc7b19;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
li.advanced-setting {
|
||||||
|
border-left: 1px solid #cc7b19;
|
||||||
|
}
|
||||||
.user-info-wrapper {
|
.user-info-wrapper {
|
||||||
}
|
}
|
||||||
.user-info-poster-face {
|
.user-info-poster-face {
|
||||||
@@ -2247,21 +2372,6 @@ a .library-user-instance-box:hover {
|
|||||||
#watched-stats-days-selection label {
|
#watched-stats-days-selection label {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
#watched-stats-days {
|
|
||||||
margin: 0;
|
|
||||||
width: 75px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
#watched-stats-count {
|
|
||||||
margin: 0;
|
|
||||||
width: 75px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
#recently-added-count {
|
|
||||||
margin: 0;
|
|
||||||
width: 75px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
.home-padded-header {
|
.home-padded-header {
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
@@ -2270,6 +2380,9 @@ a .library-user-instance-box:hover {
|
|||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
width: 175px;
|
width: 175px;
|
||||||
}
|
}
|
||||||
|
.home-padded-header .button-bar {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
.home-platforms {
|
.home-platforms {
|
||||||
}
|
}
|
||||||
.home-platforms ul,
|
.home-platforms ul,
|
||||||
@@ -3021,7 +3134,7 @@ div.dataTables_info {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
.history-thumbnail-popover {
|
.history-thumbnail-popover {
|
||||||
z-index: 2;
|
z-index: 2000;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
@@ -3110,16 +3223,16 @@ div.dataTables_info {
|
|||||||
}
|
}
|
||||||
#updatebar {
|
#updatebar {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
|
opacity: 0.95;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
display: none;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
padding: 7px 10px;
|
padding: 10px 10px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
width: 250px;
|
width: 400px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -3191,6 +3304,48 @@ pre::-webkit-scrollbar-thumb {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media only screen
|
||||||
|
and (min-device-width: 300px)
|
||||||
|
and (max-device-width: 740px) {
|
||||||
|
.header-bar {
|
||||||
|
display: block;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
.button-bar {
|
||||||
|
float: left !important;
|
||||||
|
clear: both;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.button-bar > div,
|
||||||
|
.button-bar > button,
|
||||||
|
.button-bar > span {
|
||||||
|
float: left !important;
|
||||||
|
clear: both !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.button-bar > div > button.btn {
|
||||||
|
float: left !important;
|
||||||
|
clear: both !important;
|
||||||
|
}
|
||||||
|
.home-padded-header .button-bar {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen
|
||||||
|
and (min-device-width: 740px)
|
||||||
|
and (max-device-width: 1024px) {
|
||||||
|
.button-bar {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
.button-bar > div > button.btn {
|
||||||
|
float: left !important;
|
||||||
|
clear: both !important;
|
||||||
|
}
|
||||||
|
.home-padded-header .button-bar {
|
||||||
|
float: left !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
#search_form {
|
#search_form {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
@@ -3265,22 +3420,10 @@ pre::-webkit-scrollbar-thumb {
|
|||||||
.notification-params tr:nth-child(even) td {
|
.notification-params tr:nth-child(even) td {
|
||||||
background-color: rgba(255,255,255,0.010);
|
background-color: rgba(255,255,255,0.010);
|
||||||
}
|
}
|
||||||
#days-selection label {
|
#days-selection label,
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
#graph-days {
|
|
||||||
margin: 0;
|
|
||||||
width: 75px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
#months-selection label {
|
#months-selection label {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
#graph-months {
|
|
||||||
margin: 0;
|
|
||||||
width: 75px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
.card-sortable {
|
.card-sortable {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0 20px 0 0;
|
padding: 0 20px 0 0;
|
||||||
@@ -3551,6 +3694,7 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
}
|
}
|
||||||
.git-group select.form-control {
|
.git-group select.form-control {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
#changelog-modal .modal-body > h2 {
|
#changelog-modal .modal-body > h2 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -3722,7 +3866,11 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
.no-image {
|
.no-image {
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
}
|
}
|
||||||
|
#info-modal .stream-info-current {
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
#info-modal .stream-info-item {
|
#info-modal .stream-info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -3793,3 +3941,14 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
.stream-info tr:nth-child(even) td {
|
.stream-info tr:nth-child(even) td {
|
||||||
background-color: rgba(255,255,255,0.010);
|
background-color: rgba(255,255,255,0.010);
|
||||||
}
|
}
|
||||||
|
.number-input {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 55px !important;
|
||||||
|
height: 34px !important;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
.number-input::-webkit-inner-spin-button,
|
||||||
|
.number-input::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -67,8 +67,15 @@ DOCUMENTATION :: END
|
|||||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||||
import plexpy
|
import plexpy
|
||||||
%>
|
%>
|
||||||
<% data = defaultdict(lambda: 'Unknown', **session) %>
|
<%
|
||||||
<% sk = data['session_key'] %>
|
data = defaultdict(lambda: 'Unknown', **session)
|
||||||
|
sk = data['session_key']
|
||||||
|
|
||||||
|
href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#'
|
||||||
|
parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#'
|
||||||
|
grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#'
|
||||||
|
user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#'
|
||||||
|
%>
|
||||||
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
|
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
|
||||||
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
|
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
|
||||||
<div class="dashboard-activity-container">
|
<div class="dashboard-activity-container">
|
||||||
@@ -89,15 +96,15 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
% if data['channel_stream'] == 0:
|
% if data['channel_stream'] == 0:
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<a id="poster-url-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">
|
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
|
||||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<a id="poster-url-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">
|
<a id="poster-url-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">
|
||||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<a id="poster-url-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}">
|
<a id="poster-url-${sk}" href="${parent_href}" title="${data['parent_title']}">
|
||||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif data['media_type'] in ('photo', 'clip'):
|
% elif data['media_type'] in ('photo', 'clip'):
|
||||||
@@ -201,8 +208,8 @@ DOCUMENTATION :: END
|
|||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Container</div>
|
<div class="sub-heading">Container</div>
|
||||||
<div class="sub-value" id="transcode_container-${sk}">
|
<div class="sub-value" id="transcode_container-${sk}">
|
||||||
% if data.get('stream_container_decision') == 'transcode':
|
% if data['stream_container_decision'] == 'transcode':
|
||||||
Transcode (${data['container'].upper()} → ${data['stream_container'].upper()})
|
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${data['container'].upper()})
|
Direct Play (${data['container'].upper()})
|
||||||
% endif
|
% endif
|
||||||
@@ -213,13 +220,13 @@ DOCUMENTATION :: END
|
|||||||
<div class="sub-heading">Video</div>
|
<div class="sub-heading">Video</div>
|
||||||
<div class="sub-value" id="video_decision-${sk}">
|
<div class="sub-value" id="video_decision-${sk}">
|
||||||
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
||||||
% if data.get('stream_video_decision') == 'transcode':
|
% if data['stream_video_decision'] == 'transcode':
|
||||||
<%
|
<%
|
||||||
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
||||||
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
||||||
%>
|
%>
|
||||||
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} → ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||||
% elif data.get('stream_video_decision') == 'copy':
|
% elif data['stream_video_decision'] == 'copy':
|
||||||
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
|
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
|
||||||
@@ -234,9 +241,9 @@ DOCUMENTATION :: END
|
|||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Audio</div>
|
<div class="sub-heading">Audio</div>
|
||||||
<div class="sub-value" id="audio_decision-${sk}">
|
<div class="sub-value" id="audio_decision-${sk}">
|
||||||
% if data.get('stream_audio_decision') == 'transcode':
|
% if data['stream_audio_decision'] == 'transcode':
|
||||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} → ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
% elif data.get('stream_audio_decision') == 'copy':
|
% elif data['stream_audio_decision'] == 'copy':
|
||||||
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
|
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
@@ -250,7 +257,7 @@ DOCUMENTATION :: END
|
|||||||
<div class="sub-value" id="subtitle_decision-${sk}">
|
<div class="sub-value" id="subtitle_decision-${sk}">
|
||||||
% if data['subtitles'] == 1:
|
% if data['subtitles'] == 1:
|
||||||
% if data['stream_subtitle_decision'] == 'transcode':
|
% if data['stream_subtitle_decision'] == 'transcode':
|
||||||
Transcode (${data['subtitle_codec'].upper()} → ${data['stream_subtitle_codec'].upper()})
|
Transcode (${data['subtitle_codec'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()})
|
||||||
% elif data['stream_subtitle_decision'] == 'copy':
|
% elif data['stream_subtitle_decision'] == 'copy':
|
||||||
Direct Stream (${data['subtitle_codec'].upper()})
|
Direct Stream (${data['subtitle_codec'].upper()})
|
||||||
% elif data['stream_subtitle_decision'] == 'burn':
|
% elif data['stream_subtitle_decision'] == 'burn':
|
||||||
@@ -269,18 +276,23 @@ DOCUMENTATION :: END
|
|||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Location</div>
|
<div class="sub-heading">Location</div>
|
||||||
<div class="sub-value time-right">
|
<div class="sub-value time-right">
|
||||||
|
<span id="location-${sk}">${data['location'].upper()}</span>:
|
||||||
% if data['ip_address'] != 'N/A':
|
% if data['ip_address'] != 'N/A':
|
||||||
${data['location'].upper()}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||||
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
% if data['relay']:
|
||||||
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
|
||||||
</a>
|
% else:
|
||||||
<script>
|
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
||||||
isPrivateIP("${data['ip_address']}").then(function () {
|
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
||||||
$("#external_ip-${sk}").hide();
|
</a>
|
||||||
}, function () {
|
<script>
|
||||||
$("#external_ip-${sk}").show();
|
isPrivateIP("${data['ip_address']}").then(function () {
|
||||||
});
|
$("#external_ip-${sk}").hide();
|
||||||
</script>
|
}, function () {
|
||||||
|
$("#external_ip-${sk}").show();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
% else:
|
% else:
|
||||||
N/A
|
N/A
|
||||||
% endif
|
% endif
|
||||||
@@ -312,7 +324,9 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
% if data['media_type'] != 'photo':
|
% if data['media_type'] != 'photo':
|
||||||
<div class="dashboard-activity-info-time">
|
<div class="dashboard-activity-info-time">
|
||||||
% if data['view_offset']:
|
% if data['live'] == 1:
|
||||||
|
<br />Live
|
||||||
|
% elif data['view_offset']:
|
||||||
ETA:
|
ETA:
|
||||||
<span id="stream-eta-${sk}">
|
<span id="stream-eta-${sk}">
|
||||||
<script>
|
<script>
|
||||||
@@ -340,19 +354,19 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-progress">
|
<div class="dashboard-activity-progress">
|
||||||
<div class="dashboard-activity-progress-bar">
|
<div class="dashboard-activity-progress-bar">
|
||||||
|
% if data['live'] == 1:
|
||||||
|
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-toggle="tooltip" title="Stream Progress Live">Live</div>
|
||||||
|
% else:
|
||||||
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
|
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
|
||||||
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
|
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-wrapper">
|
<div class="dashboard-activity-metadata-wrapper">
|
||||||
% if data['user_id']:
|
<a href="${user_href}" title="${data['friendly_name']}">
|
||||||
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">
|
|
||||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
||||||
</a>
|
</a>
|
||||||
% else:
|
|
||||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
|
||||||
% endif
|
|
||||||
<div class="dashboard-activity-metadata-title-container">
|
<div class="dashboard-activity-metadata-title-container">
|
||||||
<div id="play-state-${sk}" class="dashboard-activity-metadata-play_state-icon" title="${data['state'].capitalize()}">
|
<div id="play-state-${sk}" class="dashboard-activity-metadata-play_state-icon" title="${data['state'].capitalize()}">
|
||||||
% if data['state'] == 'playing':
|
% if data['state'] == 'playing':
|
||||||
@@ -365,21 +379,21 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-title">
|
<div class="dashboard-activity-metadata-title">
|
||||||
% if data['channel_stream'] == 0:
|
% if data['channel_stream'] == 0:
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
<a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<a href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
<a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||||
- <a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<a id="metadata-grandparent_title-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
<a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||||
- <a id="metadata-title-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
- <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'photo':
|
% elif data['media_type'] == 'photo':
|
||||||
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
||||||
% elif data['media_type'] == 'clip':
|
% elif data['media_type'] == 'clip':
|
||||||
<span title="${data['title']}">${data['title']}</span>
|
<span title="${data['title']}">${data['title']}</span>
|
||||||
% else:
|
% else:
|
||||||
<span title="${data['title']}">${data['title']}</span>
|
<span title="${data['title']}">${data['title']}</span>
|
||||||
% endif
|
% endif
|
||||||
% elif data['media_type'] == 'episode' and data['grandparent_title']:
|
% elif data['media_type'] == 'episode' and data['grandparent_title']:
|
||||||
<span title="${data['grandparent_title']}">${data['grandparent_title']}</span>
|
<span title="${data['grandparent_title']}">${data['grandparent_title']}</span>
|
||||||
- <span title="${data['title']}">${data['title']}</span>
|
- <span title="${data['title']}">${data['title']}</span>
|
||||||
@@ -389,7 +403,11 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-subtitle-container">
|
<div class="dashboard-activity-metadata-subtitle-container">
|
||||||
% if data['channel_stream'] == 0:
|
% if data['live'] == 1:
|
||||||
|
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Plex Live TV">
|
||||||
|
<i class="fa fa-fw fa-television"></i>
|
||||||
|
</div>
|
||||||
|
% elif data['channel_stream'] == 0:
|
||||||
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
|
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<i class="fa fa-fw fa-film"></i>
|
<i class="fa fa-fw fa-film"></i>
|
||||||
@@ -404,19 +422,21 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div id="media-type-${sk}" title="Channel">
|
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Channel">
|
||||||
<i class="fa fa-fw fa-cloud"></i>
|
<i class="fa fa-fw fa-cloud"></i>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
<div class="dashboard-activity-metadata-subtitle">
|
<div class="dashboard-activity-metadata-subtitle">
|
||||||
% if data['channel_stream'] == 0:
|
% if data['live'] == 1:
|
||||||
|
<span title="Plex Live TV" class="sub-heading">Plex Live TV</span>
|
||||||
|
% elif data['channel_stream'] == 0:
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<a href="info?rating_key=${data['parent_rating_key']}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
<a href="${parent_href}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
||||||
· <a href="info?rating_key=${data['rating_key']}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
· <a href="${href}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<a id="metadata-parent_title-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
|
<a id="metadata-parent_title-${sk}" href="${parent_href}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
|
||||||
% elif data['media_type'] == 'photo':
|
% elif data['media_type'] == 'photo':
|
||||||
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
|
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
|
||||||
% else:
|
% else:
|
||||||
@@ -441,11 +461,7 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-user">
|
<div class="dashboard-activity-metadata-user">
|
||||||
% if data['user_id']:
|
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
||||||
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
|
||||||
% else:
|
|
||||||
${data['friendly_name']}
|
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-bar-chart"></i> Graphs</span>
|
<span><i class="fa fa-bar-chart"></i> Graphs</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar hidden-xs">
|
<div class="button-bar">
|
||||||
<div class="btn-group" id="user-selection">
|
<div class="btn-group" id="user-selection">
|
||||||
<label>
|
<label>
|
||||||
<select name="graph-user" id="graph-user" class="btn" style="color: inherit;">
|
<select name="graph-user" id="graph-user" class="btn" style="color: inherit;">
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-group pull-right" style="width: 1px;" id="days-selection">
|
<div class="input-group pull-right" style="width: 1px;" id="days-selection">
|
||||||
<span class="input-group-addon btn-dark inactive">Last</span>
|
<span class="input-group-addon btn-dark inactive">Last</span>
|
||||||
<input type="number" class="form-control" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" data-default="7" data-toggle="tooltip" title="Min: 1 day" />
|
<input type="number" class="form-control number-input" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" data-default="7" data-toggle="tooltip" title="Min: 1 day" />
|
||||||
<span class="input-group-addon btn-dark inactive">days</span>
|
<span class="input-group-addon btn-dark inactive">days</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group pull-right" style="width: 1px;" id="months-selection">
|
<div class="input-group pull-right" style="width: 1px;" id="months-selection">
|
||||||
<span class="input-group-addon btn-dark inactive">Last</span>
|
<span class="input-group-addon btn-dark inactive">Last</span>
|
||||||
<input type="number" class="form-control" name="graph-months" id="graph-months" value="${config['graph_months']}" min="1" data-default="12" data-toggle="tooltip" title="Min: 1 month" />
|
<input type="number" class="form-control number-input" name="graph-months" id="graph-months" value="${config['graph_months']}" min="1" data-default="12" data-toggle="tooltip" title="Min: 1 month" />
|
||||||
<span class="input-group-addon btn-dark inactive">months</span>
|
<span class="input-group-addon btn-dark inactive">months</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_user_names',
|
url: 'get_user_names',
|
||||||
type: 'get',
|
type: 'get',
|
||||||
dataType: "json",
|
dataType: 'json',
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
var select = $('#history-user');
|
var select = $('#history-user');
|
||||||
data.sort(function (a, b) {
|
data.sort(function (a, b) {
|
||||||
@@ -130,7 +130,6 @@
|
|||||||
function loadHistoryTable(media_type, selected_user_id) {
|
function loadHistoryTable(media_type, selected_user_id) {
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
url: 'get_history',
|
url: 'get_history',
|
||||||
type: 'post',
|
|
||||||
data: function (d) {
|
data: function (d) {
|
||||||
return {
|
return {
|
||||||
json_data: JSON.stringify(d),
|
json_data: JSON.stringify(d),
|
||||||
@@ -138,9 +137,13 @@
|
|||||||
user_id: selected_user_id
|
user_id: selected_user_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
history_table = $('#history_table').DataTable(history_table_options);
|
history_table = $('#history_table').DataTable(history_table_options);
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
var colvis = new $.fn.dataTable.ColVis(history_table, {
|
||||||
|
buttonText: '<i class="fa fa-columns"></i> Select columns',
|
||||||
|
buttonClass: 'btn btn-dark',
|
||||||
|
exclude: [0, 11]
|
||||||
|
});
|
||||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
clearSearchButton('history_table', history_table);
|
clearSearchButton('history_table', history_table);
|
||||||
@@ -160,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var media_type = null;
|
var media_type = null;
|
||||||
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
|
||||||
loadHistoryTable(media_type, selected_user_id);
|
loadHistoryTable(media_type, selected_user_id);
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
|
|||||||
@@ -5,7 +5,15 @@
|
|||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
<h4 class="modal-title" id="myModalLabel">
|
<h4 class="modal-title" id="myModalLabel">
|
||||||
<strong><span id="modal_header_ip_address">
|
<strong><span id="modal_header_ip_address">
|
||||||
|
% if data.get('media_type'):
|
||||||
|
<% h = {'episode': 'TV Show', 'track': 'Music'} %>
|
||||||
|
<i class="fa fa-history"></i> ${h.get(data['media_type'], data['media_type'].title())} History for <span id="date-header">${data['start_date']}</span>
|
||||||
|
% elif data.get('transcode_decision'):
|
||||||
|
<% h = {'copy': 'Direct Stream'} %>
|
||||||
|
<i class="fa fa-history"></i> ${h.get(data['transcode_decision'], data['transcode_decision'].title())} History for <span id="date-header">${data['start_date']}</span>
|
||||||
|
% else:
|
||||||
<i class="fa fa-history"></i> History for <span id="date-header">${data['start_date']}</span>
|
<i class="fa fa-history"></i> History for <span id="date-header">${data['start_date']}</span>
|
||||||
|
% endif
|
||||||
</span></strong>
|
</span></strong>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,11 +21,18 @@
|
|||||||
<table class="display history_table" id="history_table_modal" width="100%">
|
<table class="display history_table" id="history_table_modal" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left" id="started">Started</th>
|
<th align="left" id="delete_row">Delete</th>
|
||||||
<th align="left" id="stopped">Stopped</th>
|
<th align="left" id="date">Date</th>
|
||||||
<th align="left" id="friendly_name">User</th>
|
<th align="left" id="friendly_name">User</th>
|
||||||
<th align="left" id="player">Player</th>
|
<th align="left" id="ip_address">IP Address</th>
|
||||||
|
<th align="left" id="platform">Platform</th>
|
||||||
|
<th align="left" id="device">Player</th>
|
||||||
<th align="left" id="title">Title</th>
|
<th align="left" id="title">Title</th>
|
||||||
|
<th align="left" id="started">Started</th>
|
||||||
|
<th align="left" id="paused_counter">Paused</th>
|
||||||
|
<th align="left" id="stopped">Stopped</th>
|
||||||
|
<th align="left" id="duration">Duration</th>
|
||||||
|
<th align="left" id="percent_complete"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -28,28 +43,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="${http_root}js/tables/history_table_modal.js${cache_param}"></script>
|
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#date-header').html(moment('${data["start_date"]}','YYYY-MM-DD').format('ddd MMM Do YYYY'));
|
$('#date-header').html(moment('${data["start_date"]}','YYYY-MM-DD').format('ddd MMM Do YYYY'));
|
||||||
history_table_modal_options.ajax = {
|
history_table_options.ajax = {
|
||||||
url: 'get_history',
|
url: 'get_history',
|
||||||
type: 'post',
|
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return {
|
return {
|
||||||
json_data: JSON.stringify(d),
|
json_data: JSON.stringify(d),
|
||||||
grouping: false,
|
|
||||||
user_id: "${data['user_id']}",
|
user_id: "${data['user_id']}",
|
||||||
start_date: "${data['start_date']}",
|
start_date: "${data['start_date']}",
|
||||||
media_type: "${data.get('media_type')}",
|
media_type: "${data.get('media_type')}",
|
||||||
transcode_decision: "${data.get('transcode_decision')}"
|
transcode_decision: "${data.get('transcode_decision')}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
history_table = $('#history_table_modal').DataTable(history_table_options);
|
||||||
|
history_table.columns([0, 3, 4, 8, 10, 11]).visible(false);
|
||||||
|
|
||||||
history_table = $('#history_table_modal').DataTable(history_table_modal_options);
|
|
||||||
|
|
||||||
clearSearchButton('history_table_modal', history_table);
|
clearSearchButton('history_table_modal', history_table);
|
||||||
|
|
||||||
|
$('#history-modal').on('shown.bs.modal', function() {
|
||||||
|
history_table.columns.adjust().draw();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
% else:
|
% else:
|
||||||
|
|||||||
@@ -88,17 +88,19 @@ DOCUMENTATION :: END
|
|||||||
% if stat_id in ('top_music', 'popular_music'):
|
% if stat_id in ('top_music', 'popular_music'):
|
||||||
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster-blur" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=cover);"></div>
|
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster-blur" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||||
% endif
|
% endif
|
||||||
<a id="stats-thumb-url-${stat_id}" href="info?rating_key=${row0['rating_key']}" title="${row0['title']}">
|
<% height, type = ('300', 'cover') if stat_id in ('top_music', 'popular_music') else ('450', 'poster') %>
|
||||||
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else 'poster' %>
|
<% href = 'info?rating_key={}'.format(row0['rating_key']) if row0['rating_key'] else '#' %>
|
||||||
|
<a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}">
|
||||||
% if row0['thumb']:
|
% if row0['thumb']:
|
||||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=${type});"></div>
|
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=${height}&fallback=${type});"></div>
|
||||||
% else:
|
% else:
|
||||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(images/${type}.png);"></div>
|
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(images/${type}.png);"></div>
|
||||||
% endif
|
% endif
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
% elif stat_id == 'top_users':
|
% elif stat_id == 'top_users':
|
||||||
<a id="stats-thumb-url-${stat_id}" href="user?user_id=${row0['user_id']}" title="${row0['friendly_name']}" class="hidden-xs">
|
<% user_href = 'user?user_id={}'.format(row0['user_id']) if row0['user_id'] else '#' %>
|
||||||
|
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
|
||||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif stat_id == 'top_platforms':
|
% elif stat_id == 'top_platforms':
|
||||||
@@ -127,26 +129,20 @@ DOCUMENTATION :: END
|
|||||||
% for row in top_stat['rows']:
|
% for row in top_stat['rows']:
|
||||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
|
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
|
||||||
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
||||||
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}"
|
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
|
||||||
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
|
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
|
||||||
<div class="sub-list">${loop.index + 1}</div>
|
<div class="sub-list">${loop.index + 1}</div>
|
||||||
<div class="sub-value">
|
<div class="sub-value">
|
||||||
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
||||||
% if top_stat['rows'][loop.index]['rating_key']:
|
<% href = 'info?rating_key={}'.format(row['rating_key']) if row['rating_key'] else '#' %>
|
||||||
<a href="info?rating_key=${row['rating_key']}" title="${row['title']}">
|
<a href="${href}" title="${row['title']}">
|
||||||
${row['title']}
|
${row['title']}
|
||||||
</a>
|
</a>
|
||||||
% else:
|
|
||||||
${row['title']}
|
|
||||||
% endif
|
|
||||||
% elif stat_id == 'top_users':
|
% elif stat_id == 'top_users':
|
||||||
% if top_stat['rows'][loop.index]['user_id']:
|
<% user_href = 'user?user_id={}'.format(row['user_id']) if row['user_id'] else '#' %>
|
||||||
<a href="user?user_id=${row['user_id']}" title="${row['friendly_name']}">
|
<a href="${user_href}" title="${row['friendly_name']}">
|
||||||
${row['friendly_name']}
|
${row['friendly_name']}
|
||||||
</a>
|
</a>
|
||||||
% else:
|
|
||||||
${row['friendly_name']}
|
|
||||||
% endif
|
|
||||||
% elif stat_id == 'top_platforms':
|
% elif stat_id == 'top_platforms':
|
||||||
${row['platform']}
|
${row['platform']}
|
||||||
% elif stat_id == 'most_concurrent':
|
% elif stat_id == 'most_concurrent':
|
||||||
@@ -182,13 +178,22 @@ DOCUMENTATION :: END
|
|||||||
var stat_id = $(elem).data('stat_id');
|
var stat_id = $(elem).data('stat_id');
|
||||||
var art = $(elem).data('art');
|
var art = $(elem).data('art');
|
||||||
var thumb = $(elem).data('thumb');
|
var thumb = $(elem).data('thumb');
|
||||||
|
var user_id = $(elem).data('user_id');
|
||||||
|
var user_thumb = $(elem).data('user_thumb');
|
||||||
|
var rating_key = $(elem).data('rating_key');
|
||||||
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
|
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
|
||||||
|
var href;
|
||||||
|
|
||||||
if (stat_id == 'most_concurrent') {
|
if (stat_id == 'most_concurrent') {
|
||||||
return
|
return
|
||||||
} else if (stat_id == 'top_users') {
|
} else if (stat_id == 'top_users') {
|
||||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (thumb || 'images/gravatar-default.png') + ')');
|
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
|
||||||
$('#stats-thumb-url-' + stat_id).attr('href', 'user?user_id=' + $(elem).data('user_id')).prop('title', $(elem).data('friendly_name'));
|
if (user_id) {
|
||||||
|
href = 'user?user_id=' + user_id;
|
||||||
|
} else {
|
||||||
|
href = '#';
|
||||||
|
}
|
||||||
|
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
|
||||||
} else if (stat_id == 'top_platforms') {
|
} else if (stat_id == 'top_platforms') {
|
||||||
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
||||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||||
@@ -197,7 +202,12 @@ DOCUMENTATION :: END
|
|||||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||||
}).addClass('platform-' + $(elem).data('platform'));
|
}).addClass('platform-' + $(elem).data('platform'));
|
||||||
} else {
|
} else {
|
||||||
$('#stats-thumb-url-' + stat_id).attr('href', 'info?rating_key=' + $(elem).data('rating_key')).prop('title', $(elem).data('title'));
|
if (rating_key) {
|
||||||
|
href = 'info?rating_key=' + rating_key;
|
||||||
|
} else {
|
||||||
|
href = '#';
|
||||||
|
}
|
||||||
|
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
|
||||||
if (art) {
|
if (art) {
|
||||||
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&fallback=art)');
|
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&fallback=art)');
|
||||||
} else {
|
} else {
|
||||||
@@ -207,7 +217,8 @@ DOCUMENTATION :: END
|
|||||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
|
$('#stats-thumb-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
|
||||||
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
|
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
|
||||||
} else {
|
} else {
|
||||||
$('#stats-background-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
|
$('#stats-thumb-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
|
||||||
|
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(images/' + fallback + '.png)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
data/interfaces/default/images/become_a_patron_button.png
Normal file
BIN
data/interfaces/default/images/become_a_patron_button.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 28 KiB |
BIN
data/interfaces/default/images/logo-tautulli-100.png
Normal file
BIN
data/interfaces/default/images/logo-tautulli-100.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
BIN
data/interfaces/default/images/logo-tautulli-45.png
Normal file
BIN
data/interfaces/default/images/logo-tautulli-45.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -22,7 +22,16 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="currentActivity">
|
<div id="currentActivity">
|
||||||
|
<% from plexpy import PLEX_SERVER_UP %>
|
||||||
|
% if PLEX_SERVER_UP:
|
||||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||||
|
% else:
|
||||||
|
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
|
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,27 +40,29 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="home-padded-header padded-header">
|
<div class="home-padded-header padded-header">
|
||||||
<h3 class="pull-left">Watch Statistics</h3>
|
<h3 class="pull-left">Watch Statistics</h3>
|
||||||
<div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px">
|
<div class="button-bar">
|
||||||
% if config['home_stats_type'] == 0:
|
<div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px">
|
||||||
<label class="btn btn-dark active">
|
% if config['home_stats_type'] == 0:
|
||||||
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off" checked> Play Count
|
<label class="btn btn-dark active">
|
||||||
</label>
|
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off" checked> Play Count
|
||||||
<label class="btn btn-dark">
|
</label>
|
||||||
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off"> Play Duration
|
<label class="btn btn-dark">
|
||||||
</label>
|
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off"> Play Duration
|
||||||
% else:
|
</label>
|
||||||
<label class="btn btn-dark">
|
% else:
|
||||||
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off"> Play Count
|
<label class="btn btn-dark">
|
||||||
</label>
|
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off"> Play Count
|
||||||
<label class="btn btn-dark active">
|
</label>
|
||||||
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off" checked> Play Duration
|
<label class="btn btn-dark active">
|
||||||
</label>
|
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off" checked> Play Duration
|
||||||
% endif
|
</label>
|
||||||
</div>
|
% endif
|
||||||
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection">
|
</div>
|
||||||
<span class="input-group-addon btn-dark inactive">Last</span>
|
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection">
|
||||||
<input type="number" class="form-control" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
|
<span class="input-group-addon btn-dark inactive">Last</span>
|
||||||
<span class="input-group-addon btn-dark inactive">days</span>
|
<input type="number" class="form-control number-input" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
|
||||||
|
<span class="input-group-addon btn-dark inactive">days</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +80,9 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="home-padded-header padded-header" id="library-statistics-header">
|
<div class="home-padded-header padded-header" id="library-statistics-header">
|
||||||
<h3 class="pull-left">Library Statistics</h3>
|
<h3 class="pull-left">Library Statistics</h3>
|
||||||
<span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span>
|
<div class="button-bar">
|
||||||
|
<span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,23 +107,25 @@
|
|||||||
<a href="#" id="recently-added-page-right" class="paginate btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
|
<a href="#" id="recently-added-page-right" class="paginate btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px">
|
<div class="button-bar">
|
||||||
<label class="btn btn-dark active" id="recently-added-label-all">
|
<div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px">
|
||||||
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="" autocomplete="off"> All
|
<label class="btn btn-dark active" id="recently-added-label-all">
|
||||||
</label>
|
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="" autocomplete="off"> All
|
||||||
<label class="btn btn-dark" id="recently-added-label-movies">
|
</label>
|
||||||
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies
|
<label class="btn btn-dark" id="recently-added-label-movies">
|
||||||
</label>
|
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies
|
||||||
<label class="btn btn-dark" id="recently-added-label-tv">
|
</label>
|
||||||
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows
|
<label class="btn btn-dark" id="recently-added-label-tv">
|
||||||
</label>
|
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows
|
||||||
<label class="btn btn-dark" id="recently-added-label-music">
|
</label>
|
||||||
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-music" value="artist" autocomplete="off"> Music
|
<label class="btn btn-dark" id="recently-added-label-music">
|
||||||
</label>
|
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-music" value="artist" autocomplete="off"> Music
|
||||||
</div>
|
</label>
|
||||||
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection">
|
</div>
|
||||||
<input type="number" class="form-control" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" />
|
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection">
|
||||||
<span class="input-group-addon btn-dark inactive">items</span>
|
<input type="number" class="form-control number-input" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" />
|
||||||
|
<span class="input-group-addon btn-dark inactive">items</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,12 +146,13 @@
|
|||||||
<%def name="modalIncludes()">
|
<%def name="modalIncludes()">
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
|
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
|
||||||
|
<% from plexpy.common import RELEASE %>
|
||||||
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
|
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
<h4 class="modal-title">Tautulli Updated</h4>
|
<h4 class="modal-title">Tautulli Updated to <strong>${RELEASE}</strong></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
</div>
|
</div>
|
||||||
@@ -234,9 +250,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
% if 'current_activity' in config['home_sections']:
|
<% from plexpy import PLEX_SERVER_UP %>
|
||||||
|
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
|
||||||
<script>
|
<script>
|
||||||
var defaultHandler = {
|
var defaultHandler = {
|
||||||
get: function(target, name) {
|
get: function(target, name) {
|
||||||
@@ -259,6 +276,7 @@
|
|||||||
async: true,
|
async: true,
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.log(status + ': ' + error);
|
console.log(status + ': ' + error);
|
||||||
|
activity_ready = true;
|
||||||
},
|
},
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
$('#dashboard-checking-activity').remove();
|
$('#dashboard-checking-activity').remove();
|
||||||
@@ -273,9 +291,9 @@
|
|||||||
|
|
||||||
if (!(current_activity)) {
|
if (!(current_activity)) {
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
var msg_settings = ' Verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
|
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
|
||||||
% else:
|
% else:
|
||||||
var msg_settings = ''
|
var msg_settings = '';
|
||||||
% endif
|
% endif
|
||||||
$('#currentActivityHeader').hide();
|
$('#currentActivityHeader').hide();
|
||||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
|
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
|
||||||
@@ -292,7 +310,9 @@
|
|||||||
var sc_dp = current_activity.stream_count_direct_play,
|
var sc_dp = current_activity.stream_count_direct_play,
|
||||||
sc_ds = current_activity.stream_count_direct_stream,
|
sc_ds = current_activity.stream_count_direct_stream,
|
||||||
sc_tc = current_activity.stream_count_transcode,
|
sc_tc = current_activity.stream_count_transcode,
|
||||||
total_bw = current_activity.total_bandwidth;
|
total_bw = current_activity.total_bandwidth,
|
||||||
|
lan_bw = current_activity.lan_bandwidth,
|
||||||
|
wan_bw = current_activity.wan_bandwidth;
|
||||||
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
||||||
if (sc_dp) {
|
if (sc_dp) {
|
||||||
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
||||||
@@ -306,13 +326,23 @@
|
|||||||
streams_header = streams_header.replace(/, $/, '') + ')';
|
streams_header = streams_header.replace(/, $/, '') + ')';
|
||||||
$('#currentActivityHeader-streams').text(streams_header);
|
$('#currentActivityHeader-streams').text(streams_header);
|
||||||
|
|
||||||
var bandwidth_header = (total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps');
|
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps'));
|
||||||
|
var lan_wan_bandwidth_header = '';
|
||||||
|
if (lan_bw) {
|
||||||
|
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
||||||
|
}
|
||||||
|
if (wan_bw) {
|
||||||
|
lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
||||||
|
}
|
||||||
|
if (lan_wan_bandwidth_header) {
|
||||||
|
bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')';
|
||||||
|
}
|
||||||
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
||||||
|
|
||||||
$('#currentActivityHeader').show();
|
$('#currentActivityHeader').show();
|
||||||
|
|
||||||
sessions.forEach(function (session) {
|
sessions.forEach(function (session) {
|
||||||
var s = new Proxy(session, defaultHandler);
|
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
|
||||||
var key = s.session_key;
|
var key = s.session_key;
|
||||||
var session_id = s.session_id;
|
var session_id = s.session_id;
|
||||||
var instance = $('#activity-instance-' + key);
|
var instance = $('#activity-instance-' + key);
|
||||||
@@ -386,7 +416,7 @@
|
|||||||
|
|
||||||
var transcode_container = '';
|
var transcode_container = '';
|
||||||
if (s.stream_container_decision === 'transcode') {
|
if (s.stream_container_decision === 'transcode') {
|
||||||
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' → ' + s.stream_container.toUpperCase() + ')';
|
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
|
||||||
} else {
|
} else {
|
||||||
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
|
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
|
||||||
}
|
}
|
||||||
@@ -419,7 +449,7 @@
|
|||||||
if (s.stream_video_decision === 'transcode') {
|
if (s.stream_video_decision === 'transcode') {
|
||||||
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
||||||
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
||||||
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' → ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
||||||
} else if (s.stream_video_decision === 'copy') {
|
} else if (s.stream_video_decision === 'copy') {
|
||||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
||||||
} else {
|
} else {
|
||||||
@@ -435,7 +465,7 @@
|
|||||||
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
||||||
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
||||||
if (s.stream_audio_decision === 'transcode') {
|
if (s.stream_audio_decision === 'transcode') {
|
||||||
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' → ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' <i class="fa fa-long-arrow-right"></i> ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||||
} else if (s.stream_audio_decision === 'copy') {
|
} else if (s.stream_audio_decision === 'copy') {
|
||||||
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||||
} else {
|
} else {
|
||||||
@@ -447,7 +477,7 @@
|
|||||||
var subtitle_decision = 'None';
|
var subtitle_decision = 'None';
|
||||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
|
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
|
||||||
if (s.stream_subtitle_decision === 'transcode') {
|
if (s.stream_subtitle_decision === 'transcode') {
|
||||||
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' → ' + s.stream_subtitle_codec.toUpperCase() + ')';
|
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')';
|
||||||
} else if (s.stream_subtitle_decision === 'copy') {
|
} else if (s.stream_subtitle_decision === 'copy') {
|
||||||
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
|
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
|
||||||
} else if (s.stream_subtitle_decision === 'burn') {
|
} else if (s.stream_subtitle_decision === 'burn') {
|
||||||
@@ -475,6 +505,8 @@
|
|||||||
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
|
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
|
||||||
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
|
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
|
||||||
|
|
||||||
|
$('#location-' + key).html(s.location.toUpperCase());
|
||||||
|
|
||||||
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
|
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
|
||||||
var bw = parseInt(s.bandwidth);
|
var bw = parseInt(s.bandwidth);
|
||||||
if (bw !== "Unknown") {
|
if (bw !== "Unknown") {
|
||||||
@@ -500,7 +532,7 @@
|
|||||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||||
var progress_bar = $('#progress-bar-' + key);
|
var progress_bar = $('#progress-bar-' + key);
|
||||||
progress_bar.data('state', s.state);
|
progress_bar.data('state', s.state);
|
||||||
if (progress_bar.data('last_view_offset') !== s.view_offset) {
|
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
|
||||||
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,7 +840,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_changelog',
|
url: 'get_changelog',
|
||||||
data: {
|
data: {
|
||||||
latest_only: true,
|
since_prev_release: true,
|
||||||
update_shown: true
|
update_shown: true
|
||||||
},
|
},
|
||||||
cache: false,
|
cache: false,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ DOCUMENTATION :: END
|
|||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -117,9 +117,9 @@ DOCUMENTATION :: END
|
|||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||||
% if data['media_type'] == 'track':
|
% if data['media_type'] == 'track':
|
||||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
|
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
|
||||||
% else:
|
% else:
|
||||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
|
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
|
||||||
% endif
|
% endif
|
||||||
% if data['media_type'] == 'episode':
|
% if data['media_type'] == 'episode':
|
||||||
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
|
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
|
||||||
@@ -388,6 +388,15 @@ DOCUMENTATION :: END
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
% if data.get('tvmaze_id') or data.get('themoviedb_id'):
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-lookup-info"
|
||||||
|
data-id="${data['grandparent_rating_key'] if data['media_type'] in ('episode', 'track') else data['parent_rating_key'] if data['media_type'] in ('season', 'album') else data['rating_key']}"
|
||||||
|
data-title="${data['grandparent_title'] if data['media_type'] in ('episode', 'track') else data['parent_title'] if data['media_type'] in ('season', 'album') else data['title']}">
|
||||||
|
<i class="fa fa-search"></i> Delete Lookup Info
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
% if data.get('poster_url'):
|
% if data.get('poster_url'):
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
% if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
% if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||||
@@ -396,8 +405,9 @@ DOCUMENTATION :: END
|
|||||||
<span class="imgur-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="120" data-width="80" style="display: inline-flex;">
|
<span class="imgur-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="120" data-width="80" style="display: inline-flex;">
|
||||||
% endif
|
% endif
|
||||||
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-imgur-poster"
|
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-imgur-poster"
|
||||||
data-id="${data['parent_rating_key'] if data['media_type'] in ('episode', 'track') else data['rating_key']}">
|
data-id="${data['parent_rating_key'] if data['media_type'] in ('episode', 'track') else data['rating_key']}"
|
||||||
<i class="fa fa-picture-o"></i> Reset Imgur Poster
|
data-title="${data["poster_title"]}">
|
||||||
|
<i class="fa fa-picture-o"></i> Delete Imgur Poster
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -511,6 +521,7 @@ DOCUMENTATION :: END
|
|||||||
% endfor
|
% endfor
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help-block">Note: All custom notification conditions will be bypassed.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -541,7 +552,7 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
grandparent_rating_key: "${data['rating_key']}",
|
grandparent_rating_key: "${data['rating_key']}",
|
||||||
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +568,7 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
parent_rating_key: "${data['rating_key']}",
|
parent_rating_key: "${data['rating_key']}",
|
||||||
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,7 +584,7 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
rating_key: "${data['rating_key']}",
|
rating_key: "${data['rating_key']}",
|
||||||
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,13 +717,28 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#delete-imgur-poster').on('click', function () {
|
$('#delete-imgur-poster').on('click', function () {
|
||||||
var msg = 'Are you sure you want to reset the Imgur poster for <strong>${data["poster_title"]}</strong>?';
|
var msg = 'Are you sure you want to delete the Imgur poster for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
|
||||||
var url = 'delete_poster_url';
|
'All previous links to this image will no longer work.';
|
||||||
var data = { rating_key: $(this).data('id') }
|
var url = 'delete_imgur_poster';
|
||||||
|
var data = { rating_key: $(this).data('id') };
|
||||||
var callback = function () {
|
var callback = function () {
|
||||||
$('.imgur-poster-tooltip').popover('destroy');
|
$('.imgur-poster-tooltip').popover('destroy');
|
||||||
$('#delete-imgur-poster').closest('span').remove();
|
$('#delete-imgur-poster').closest('.btn-group').remove();
|
||||||
}
|
};
|
||||||
|
confirmAjaxCall(url, msg, data, false, callback);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
% if data.get('tvmaze_id') or data.get('themoviedb_id'):
|
||||||
|
<script>
|
||||||
|
$('#delete-lookup-info').on('click', function () {
|
||||||
|
var msg = 'Are you sure you want to delete the 3rd party API lookup for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
|
||||||
|
'The info will be looked up again the next time a notification is sent.';
|
||||||
|
var url = 'delete_lookup_info';
|
||||||
|
var data = { rating_key: $(this).data('id'), title: $(this).data('title') };
|
||||||
|
var callback = function () {
|
||||||
|
$('#delete-lookup-info').closest('.btn-group').remove();
|
||||||
|
};
|
||||||
confirmAjaxCall(url, msg, data, false, callback);
|
confirmAjaxCall(url, msg, data, false, callback);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
PNotify.prototype.options.addclass = "stack-bottomright";
|
PNotify.prototype.options.addclass = "stack-bottomright";
|
||||||
PNotify.prototype.options.buttons.closer_hover = false;
|
PNotify.prototype.options.buttons.closer_hover = false;
|
||||||
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo.png' }
|
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo-circle.png' };
|
||||||
PNotify.prototype.options.history = false;
|
PNotify.prototype.options.history = false;
|
||||||
PNotify.prototype.options.shadow = false;
|
PNotify.prototype.options.shadow = false;
|
||||||
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
|
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
|
||||||
@@ -21,7 +21,7 @@ function check_notifications() {
|
|||||||
$.getJSON('get_browser_notifications', function (data) {
|
$.getJSON('get_browser_notifications', function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$.each(data, function (i, notification) {
|
$.each(data, function (i, notification) {
|
||||||
if (notification.delay == 0) {
|
if (notification.delay === 0) {
|
||||||
PNotify.prototype.options.hide = false;
|
PNotify.prototype.options.hide = false;
|
||||||
} else {
|
} else {
|
||||||
PNotify.prototype.options.hide = true;
|
PNotify.prototype.options.hide = true;
|
||||||
@@ -34,7 +34,7 @@ function check_notifications() {
|
|||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
check_notifications();
|
check_notifications();
|
||||||
}, 3000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function refreshTab() {
|
|||||||
|
|
||||||
function showMsg(msg, loader, timeout, ms, error) {
|
function showMsg(msg, loader, timeout, ms, error) {
|
||||||
var feedback = $("#ajaxMsg");
|
var feedback = $("#ajaxMsg");
|
||||||
update = $("#updatebar");
|
var update = $("#updatebar");
|
||||||
if (update.is(":visible")) {
|
if (update.is(":visible")) {
|
||||||
var height = update.height() + 35;
|
var height = update.height() + 35;
|
||||||
feedback.css("bottom", height + "px");
|
feedback.css("bottom", height + "px");
|
||||||
@@ -35,7 +35,7 @@ function showMsg(msg, loader, timeout, ms, error) {
|
|||||||
}
|
}
|
||||||
var message = $("<div class='msg'>" + msg + "</div>");
|
var message = $("<div class='msg'>" + msg + "</div>");
|
||||||
if (loader) {
|
if (loader) {
|
||||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||||
feedback.css("padding", "14px 10px")
|
feedback.css("padding", "14px 10px")
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -290,19 +290,13 @@ String.prototype.toProperCase = function () {
|
|||||||
|
|
||||||
function millisecondsToMinutes(ms, roundToMinute) {
|
function millisecondsToMinutes(ms, roundToMinute) {
|
||||||
if (ms > 0) {
|
if (ms > 0) {
|
||||||
seconds = ms / 1000;
|
var minutes = Math.floor(ms / 60000);
|
||||||
minutes = seconds / 60;
|
var seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||||
if (roundToMinute) {
|
if (roundToMinute) {
|
||||||
output = Math.round(minutes, 0)
|
return (seconds >= 30 ? (minutes + 1) : minutes);
|
||||||
} else {
|
} else {
|
||||||
minutesFloor = Math.floor(minutes);
|
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
|
||||||
secondsReal = Math.round((seconds - (minutesFloor * 60)), 0);
|
}
|
||||||
if (secondsReal < 10) {
|
|
||||||
secondsReal = '0' + secondsReal;
|
|
||||||
}
|
|
||||||
output = minutesFloor + ':' + secondsReal;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
} else {
|
} else {
|
||||||
if (roundToMinute) {
|
if (roundToMinute) {
|
||||||
return '0';
|
return '0';
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ history_table_options = {
|
|||||||
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||||
"emptyTable": "No data in table",
|
"emptyTable": "No data in table",
|
||||||
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
||||||
},
|
},
|
||||||
"pagingType": "full_numbers",
|
"pagingType": "full_numbers",
|
||||||
"stateSave": true,
|
"stateSave": true,
|
||||||
"processing": false,
|
"processing": false,
|
||||||
@@ -172,7 +172,7 @@ history_table_options = {
|
|||||||
},
|
},
|
||||||
"width": "33%",
|
"width": "33%",
|
||||||
"className": "datatable-wrap"
|
"className": "datatable-wrap"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [7],
|
"targets": [7],
|
||||||
"data":"started",
|
"data":"started",
|
||||||
@@ -270,7 +270,7 @@ history_table_options = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ($('#row-edit-mode').hasClass('active')) {
|
if ($('#row-edit-mode').hasClass('active')) {
|
||||||
$('.delete-control').each(function () {
|
$('.history_table .delete-control').each(function () {
|
||||||
$(this).removeClass('hidden');
|
$(this).removeClass('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -290,7 +290,9 @@ history_table_options = {
|
|||||||
},
|
},
|
||||||
"preDrawCallback": function(settings) {
|
"preDrawCallback": function(settings) {
|
||||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0);
|
||||||
|
$('[data-toggle="tooltip"]').tooltip('destroy');
|
||||||
|
$('[data-toggle="popover"]').popover('destroy');
|
||||||
},
|
},
|
||||||
"rowCallback": function (row, rowData, rowIndex) {
|
"rowCallback": function (row, rowData, rowIndex) {
|
||||||
if (rowData['group_count'] == 1) {
|
if (rowData['group_count'] == 1) {
|
||||||
@@ -322,7 +324,7 @@ history_table_options = {
|
|||||||
$(row).addClass('current-activity-row');
|
$(row).addClass('current-activity-row');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Parent table platform modal
|
// Parent table platform modal
|
||||||
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||||
@@ -464,7 +466,7 @@ function childTableOptions(rowData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ($('#row-edit-mode').hasClass('active')) {
|
if ($('#row-edit-mode').hasClass('active')) {
|
||||||
$('.delete-control').each(function () {
|
$('.history_table .delete-control').each(function () {
|
||||||
$(this).removeClass('hidden');
|
$(this).removeClass('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ login_log_table_options = {
|
|||||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$('.login_log_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
|
$('.login_log_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
|
||||||
var tr = $(this).closest('tr');
|
var tr = $(this).closest('tr');
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ media_info_table_options = {
|
|||||||
} else if (rowData['media_type'] === 'album') {
|
} else if (rowData['media_type'] === 'album') {
|
||||||
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
||||||
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'photo' && rowData['parent_rating_key'] == '') {
|
} else if (rowData['media_type'] === 'photo_album') {
|
||||||
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
||||||
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
||||||
} else {
|
} else {
|
||||||
@@ -77,32 +77,44 @@ media_info_table_options = {
|
|||||||
if (rowData['media_type'] === 'movie') {
|
if (rowData['media_type'] === 'movie') {
|
||||||
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'show') {
|
} else if (rowData['media_type'] === 'show') {
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'season') {
|
} else if (rowData['media_type'] === 'season') {
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Season"><i class="fa fa-television fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Season"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'episode') {
|
} else if (rowData['media_type'] === 'episode') {
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'artist') {
|
} else if (rowData['media_type'] === 'artist') {
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Artist"><i class="fa fa-music fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Artist"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'album') {
|
} else if (rowData['media_type'] === 'album') {
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Album"><i class="fa fa-music fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Album"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'track') {
|
} else if (rowData['media_type'] === 'track') {
|
||||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>'
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
|
||||||
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'photo_album') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||||
|
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||||
|
} else if (rowData['media_type'] === 'photo') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
|
||||||
|
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||||
|
} else if (rowData['media_type'] === 'clip') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">' + rowData['title'] + '</span>';
|
||||||
|
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html(cellData);
|
$(td).html(cellData);
|
||||||
}
|
}
|
||||||
@@ -335,7 +347,7 @@ function childTableOptionsMedia(rowData) {
|
|||||||
case 'album':
|
case 'album':
|
||||||
section_type = 'track';
|
section_type = 'track';
|
||||||
break;
|
break;
|
||||||
case 'photo':
|
case 'photo_album':
|
||||||
section_type = 'picture';
|
section_type = 'picture';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ sync_table_options = {
|
|||||||
"data": "total_size",
|
"data": "total_size",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData > 0 ) {
|
if (cellData > 0 ) {
|
||||||
megabytes = Math.round((cellData/1024)/1024, 0)
|
megabytes = Math.round((cellData/1024)/1024, 0);
|
||||||
$(td).html(megabytes + 'MB');
|
$(td).html(megabytes + 'MB');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('0MB');
|
$(td).html('0MB');
|
||||||
@@ -139,19 +139,28 @@ sync_table_options = {
|
|||||||
// $('html,body').scrollTop(0);
|
// $('html,body').scrollTop(0);
|
||||||
|
|
||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
|
if ($('#sync-row-edit-mode').hasClass('active')) {
|
||||||
|
$('.sync_table .delete-control').each(function () {
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"preDrawCallback": function (settings) {
|
"preDrawCallback": function (settings) {
|
||||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$('#sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
|
$('.sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
|
||||||
var tr = $(this).parents('tr');
|
var tr = $(this).parents('tr');
|
||||||
var row = sync_table.row(tr);
|
var row = sync_table.row(tr);
|
||||||
var rowData = row.data();
|
var rowData = row.data();
|
||||||
|
|
||||||
var index_delete = syncs_to_delete.findIndex(x => x.client_id == rowData['client_id'] && x.sync_id == rowData['sync_id']);
|
var index_delete = syncs_to_delete.findIndex(function (x) {
|
||||||
|
return x.client_id === rowData['client_id'] && x.sync_id === rowData['sync_id'];
|
||||||
|
});
|
||||||
|
|
||||||
if (index_delete === -1) {
|
if (index_delete === -1) {
|
||||||
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });
|
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -180,18 +180,20 @@
|
|||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$("#refresh-libraries-list").click(function () {
|
$("#refresh-libraries-list").click(function () {
|
||||||
|
showMsg('Refreshing libraries list...', true, false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'refresh_libraries_list',
|
url: 'refresh_libraries_list',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success: function (data) {
|
complete: function (xhr, status) {
|
||||||
showMsg('<i class="fa fa-refresh"></i> Libraries list refresh started...', false, true, 2000, false);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
},
|
var msg = result.message;
|
||||||
complete: function (data) {
|
if (result.result == 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> Libraries list refreshed.', false, true, 2000, false);
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||||
},
|
libraries_list_table.draw();
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
} else {
|
||||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh libraries list.', false, true, 2000, true);
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ DOCUMENTATION :: END
|
|||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -379,10 +379,10 @@ DOCUMENTATION :: END
|
|||||||
return {
|
return {
|
||||||
json_data: JSON.stringify( d ),
|
json_data: JSON.stringify( d ),
|
||||||
section_id: section_id,
|
section_id: section_id,
|
||||||
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
|
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
|
||||||
|
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||||
@@ -392,7 +392,13 @@ DOCUMENTATION :: END
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
||||||
loadHistoryTable();
|
if (typeof(history_table) === 'undefined') {
|
||||||
|
loadHistoryTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-history-list").click(function () {
|
||||||
|
history_table.draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -408,7 +414,7 @@ DOCUMENTATION :: END
|
|||||||
refresh: refresh_table
|
refresh: refresh_table
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
||||||
|
|
||||||
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||||
@@ -418,7 +424,9 @@ DOCUMENTATION :: END
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
|
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
|
||||||
loadMediaInfoTable();
|
if (typeof(media_info_table) === 'undefined') {
|
||||||
|
loadMediaInfoTable();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#refresh-media-info-table").click(function () {
|
$("#refresh-media-info-table").click(function () {
|
||||||
@@ -484,10 +492,6 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
$("#refresh-history-list").click(function () {
|
|
||||||
history_table.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
function recentlyWatched() {
|
function recentlyWatched() {
|
||||||
// Populate recently watched
|
// Populate recently watched
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ DOCUMENTATION :: END
|
|||||||
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
|
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
|
||||||
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
|
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
|
||||||
'artist': ('Music Libraries', ('Artists', 'Albums', 'Tracks')),
|
'artist': ('Music Libraries', ('Artists', 'Albums', 'Tracks')),
|
||||||
'photo': ('Photo Libraries', ('Albums', '', 'Photos'))}
|
'photo': ('Photo Libraries', ('Albums', 'Photos', 'Videos'))}
|
||||||
%>
|
%>
|
||||||
% for section_type in types:
|
% for section_type in types:
|
||||||
% if section_type in data:
|
% if section_type in data:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
||||||
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
|
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
|
||||||
@@ -37,21 +37,19 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-logo">
|
<div class="login-logo">
|
||||||
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 100px;"></object>
|
<img src="${http_root}images/logo-tautulli-100.png" height="100" alt="PlexPy">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 col-sm-offset-3">
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
<form action="${http_root}auth/login" method="post">
|
<form id="login-form">
|
||||||
% if msg:
|
<div id="incorrect-login" class="alert alert-danger" style="text-align: center; padding: 8px; display: none;">
|
||||||
<div class="alert alert-danger" style="text-align: center; padding: 8px;">
|
Incorrect username or password.
|
||||||
${msg}
|
|
||||||
</div>
|
</div>
|
||||||
% endif
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username" class="control-label">
|
<label for="username" class="control-label">
|
||||||
Username
|
Username
|
||||||
</label>
|
</label>
|
||||||
<input type="text" id="username" name="username" class="form-control" autocorrect="off" autocapitalize="off" value="${username}" autofocus>
|
<input type="text" id="username" name="username" class="form-control" autocorrect="off" autocapitalize="off" autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password" class="control-label">
|
<label for="password" class="control-label">
|
||||||
@@ -65,7 +63,7 @@
|
|||||||
<input type="checkbox" id="remember_me" name="remember_me" title="for 30 days" value="1" checked="checked" /> Remember me
|
<input type="checkbox" id="remember_me" name="remember_me" title="for 30 days" value="1" checked="checked" /> Remember me
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,5 +73,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$('#login-form').submit(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i> Sign In');
|
||||||
|
$.ajax({
|
||||||
|
url: '${http_root}auth/signin',
|
||||||
|
type: 'POST',
|
||||||
|
data: $(this).serialize(),
|
||||||
|
dataType: 'json',
|
||||||
|
statusCode: {
|
||||||
|
200: function() {
|
||||||
|
window.location = "${http_root}";
|
||||||
|
},
|
||||||
|
401: function() {
|
||||||
|
$('#incorrect-login').show();
|
||||||
|
$('#username').focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i> Sign In');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
<style>
|
<style>
|
||||||
td {word-break: break-all;}
|
td {word-break: break-all;}
|
||||||
</style>
|
</style>
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
<span><i class="fa fa-list-alt"></i> Logs</span>
|
<span><i class="fa fa-list-alt"></i> Logs</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<div class="btn-group" id="plexpy-log-levels">
|
<div class="btn-group" id="tautulli-log-levels">
|
||||||
<label>
|
<label>
|
||||||
<select name="plexpy-log-level-filter" id="plexpy-log-level-filter" class="btn" style="color: inherit;">
|
<select name="tautulli-log-level-filter" id="tautulli-log-level-filter" class="btn" style="color: inherit;">
|
||||||
<option value="">All log levels</option>
|
<option value="">All log levels</option>
|
||||||
<option disabled>────────────</option>
|
<option disabled>────────────</option>
|
||||||
<option value="DEBUG">Debug</option>
|
<option value="DEBUG">Debug</option>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-dark" id="download-plexpylog"><i class="fa fa-download"></i> Download logs</button>
|
<button class="btn btn-dark" id="download-tautullilog"><i class="fa fa-download"></i> Download logs</button>
|
||||||
<button class="btn btn-dark" id="download-plexserverlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
|
<button class="btn btn-dark" id="download-plexserverlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
|
||||||
<button class="btn btn-dark" id="download-plexscannerlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
|
<button class="btn btn-dark" id="download-plexscannerlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
|
||||||
<button class="btn btn-dark" id="clear-logs"><i class="fa fa-trash-o"></i> Clear logs</button>
|
<button class="btn btn-dark" id="clear-logs"><i class="fa fa-trash-o"></i> Clear logs</button>
|
||||||
@@ -56,17 +56,17 @@
|
|||||||
<div class='table-card-back'>
|
<div class='table-card-back'>
|
||||||
<div>
|
<div>
|
||||||
<ul id="log_tabs" class="nav nav-pills" role="tablist">
|
<ul id="log_tabs" class="nav nav-pills" role="tablist">
|
||||||
<li role="presentation" class="active"><a id="plexpy-logs-btn" href="#tabs-plexpy_log" aria-controls="tabs-plexpy_log" role="tab" data-toggle="tab">Tautulli Logs</a></li>
|
<li role="presentation" class="active"><a id="tautulli-logs-btn" href="#tabs-tautulli_log" aria-controls="tabs-tautulli_log" role="tab" data-toggle="tab">Tautulli Logs</a></li>
|
||||||
<li role="presentation"><a id="plexpy-api-logs-btn" href="#tabs-plexpy_api_log" aria-controls="tabs-plexpy_api_log" role="tab" data-toggle="tab">Tautulli API Logs</a></li>
|
<li role="presentation"><a id="tautulli-api-logs-btn" href="#tabs-tautulli_api_log" aria-controls="tabs-tautulli_api_log" role="tab" data-toggle="tab">Tautulli API Logs</a></li>
|
||||||
<li role="presentation"><a id="plex-logs-btn" href="#tabs-plex_log" aria-controls="tabs-plex_log" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
|
<li role="presentation"><a id="plex-logs-btn" href="#tabs-plex_log" aria-controls="tabs-plex_log" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
|
||||||
<li role="presentation"><a id="plex-scanner-logs-btn" href="#tabs-plex_scanner_log" aria-controls="tabs-plex_scanner_log" role="tab" data-toggle="tab">Plex Media Scanner Logs</a></li>
|
<li role="presentation"><a id="plex-scanner-logs-btn" href="#tabs-plex_scanner_log" aria-controls="tabs-plex_scanner_log" role="tab" data-toggle="tab">Plex Media Scanner Logs</a></li>
|
||||||
<li role="presentation"><a id="plexpy-websocket-logs-btn" href="#tabs-plex_websocket_log" aria-controls="tabs-plex_websocket_log" role="tab" data-toggle="tab">Plex Websocket Logs</a></li>
|
<li role="presentation"><a id="plex-websocket-logs-btn" href="#tabs-plex_websocket_log" aria-controls="tabs-plex_websocket_log" role="tab" data-toggle="tab">Plex Websocket Logs</a></li>
|
||||||
<li role="presentation"><a id="notification-logs-btn" href="#tabs-notification_log" aria-controls="tabs-notification_log" role="tab" data-toggle="tab">Notification Logs</a></li>
|
<li role="presentation"><a id="notification-logs-btn" href="#tabs-notification_log" aria-controls="tabs-notification_log" role="tab" data-toggle="tab">Notification Logs</a></li>
|
||||||
<li role="presentation"><a id="login-logs-btn" href="#tabs-login_log" aria-controls="tabs-login_log" role="tab" data-toggle="tab">Login Logs</a></li>
|
<li role="presentation"><a id="login-logs-btn" href="#tabs-login_log" aria-controls="tabs-login_log" role="tab" data-toggle="tab">Login Logs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="tabs-plexpy_log" data-logfile="plexpy">
|
<div role="tabpanel" class="tab-pane active" id="tabs-tautulli_log" data-logfile="tautulli">
|
||||||
<table class="display" id="plexpy_log_table" width="100%">
|
<table class="display" id="tautulli_log_table" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
|
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
|
||||||
@@ -77,8 +77,8 @@
|
|||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-plexpy_api_log" data-logfile="plexpy_api">
|
<div role="tabpanel" class="tab-pane" id="tabs-tautulli_api_log" data-logfile="tautulli_api">
|
||||||
<table class="display" id="plexpy_api_log_table" width="100%">
|
<table class="display" id="tautulli_api_log_table" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
|
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
|
||||||
@@ -195,8 +195,8 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
loadPlexPyLogs('plexpy', selected_log_level);
|
loadtautullilogs('tautulli', selected_log_level);
|
||||||
clearSearchButton('plexpy_log_table', log_table);
|
clearSearchButton('tautulli_log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
var log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
var log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selected_log_level = null;
|
var selected_log_level = null;
|
||||||
function loadPlexPyLogs(logfile, selected_log_level) {
|
function loadtautullilogs(logfile, selected_log_level) {
|
||||||
log_table_options.ajax = {
|
log_table_options.ajax = {
|
||||||
url: "get_log",
|
url: "get_log",
|
||||||
type: 'post',
|
type: 'post',
|
||||||
@@ -238,10 +238,10 @@
|
|||||||
log_level: selected_log_level
|
log_level: selected_log_level
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
log_table = $('#' + logfile + '_log_table').DataTable(log_table_options);
|
log_table = $('#' + logfile + '_log_table').DataTable(log_table_options);
|
||||||
|
|
||||||
$('#plexpy-log-level-filter').on('change', function () {
|
$('#tautulli-log-level-filter').on('change', function () {
|
||||||
selected_log_level = $(this).val() || null;
|
selected_log_level = $(this).val() || null;
|
||||||
log_table.draw();
|
log_table.draw();
|
||||||
});
|
});
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
function loadPlexLogs() {
|
function loadPlexLogs() {
|
||||||
plex_log_table_options.ajax = {
|
plex_log_table_options.ajax = {
|
||||||
url: "get_plex_log?log_type=server"
|
url: "get_plex_log?log_type=server"
|
||||||
}
|
};
|
||||||
plex_log_table_options.initComplete = bindLogLevelFilter;
|
plex_log_table_options.initComplete = bindLogLevelFilter;
|
||||||
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
|
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
function loadPlexScannerLogs() {
|
function loadPlexScannerLogs() {
|
||||||
plex_log_table_options.ajax = {
|
plex_log_table_options.ajax = {
|
||||||
url: "get_plex_log?log_type=scanner"
|
url: "get_plex_log?log_type=scanner"
|
||||||
}
|
};
|
||||||
plex_log_table_options.initComplete = bindLogLevelFilter;
|
plex_log_table_options.initComplete = bindLogLevelFilter;
|
||||||
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
|
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
json_data: JSON.stringify(d)
|
json_data: JSON.stringify(d)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
notification_log_table = $('#notification_log_table').DataTable(notification_log_table_options);
|
notification_log_table = $('#notification_log_table').DataTable(notification_log_table_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,56 +284,56 @@
|
|||||||
json_data: JSON.stringify(d)
|
json_data: JSON.stringify(d)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
login_log_table = $('#login_log_table').DataTable(login_log_table_options);
|
login_log_table = $('#login_log_table').DataTable(login_log_table_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#plexpy-logs-btn").click(function () {
|
$("#tautulli-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").show();
|
$("#tautulli-log-levels").show();
|
||||||
$("#plex-log-levels").hide();
|
$("#plex-log-levels").hide();
|
||||||
$("#clear-logs").show();
|
$("#clear-logs").show();
|
||||||
$("#download-plexpylog").show()
|
$("#download-tautullilog").show();
|
||||||
$("#download-plexserverlog").hide()
|
$("#download-plexserverlog").hide();
|
||||||
$("#download-plexscannerlog").hide()
|
$("#download-plexscannerlog").hide();
|
||||||
$("#clear-notify-logs").hide();
|
$("#clear-notify-logs").hide();
|
||||||
$("#clear-login-logs").hide();
|
$("#clear-login-logs").hide();
|
||||||
loadPlexPyLogs('plexpy', selected_log_level);
|
loadtautullilogs('tautulli', selected_log_level);
|
||||||
clearSearchButton('plexpy_log_table', log_table);
|
clearSearchButton('tautulli_log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#plexpy-api-logs-btn").click(function () {
|
$("#tautulli-api-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").show();
|
$("#tautulli-log-levels").show();
|
||||||
$("#plex-log-levels").hide();
|
$("#plex-log-levels").hide();
|
||||||
$("#clear-logs").show();
|
$("#clear-logs").show();
|
||||||
$("#download-plexpylog").show()
|
$("#download-tautullilog").show();
|
||||||
$("#download-plexserverlog").hide()
|
$("#download-plexserverlog").hide();
|
||||||
$("#download-plexscannerlog").hide()
|
$("#download-plexscannerlog").hide();
|
||||||
$("#clear-notify-logs").hide();
|
$("#clear-notify-logs").hide();
|
||||||
$("#clear-login-logs").hide();
|
$("#clear-login-logs").hide();
|
||||||
loadPlexPyLogs('plexpy_api', selected_log_level);
|
loadtautullilogs('tautulli_api', selected_log_level);
|
||||||
clearSearchButton('plexpy_api_log_table', log_table);
|
clearSearchButton('tautulli_api_log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#plexpy-websocket-logs-btn").click(function () {
|
$("#plex-websocket-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").show();
|
$("#tautulli-log-levels").show();
|
||||||
$("#plex-log-levels").hide();
|
$("#plex-log-levels").hide();
|
||||||
$("#clear-logs").show();
|
$("#clear-logs").show();
|
||||||
$("#download-plexpylog").show()
|
$("#download-tautullilog").show();
|
||||||
$("#download-plexserverlog").hide()
|
$("#download-plexserverlog").hide();
|
||||||
$("#download-plexscannerlog").hide()
|
$("#download-plexscannerlog").hide();
|
||||||
$("#clear-notify-logs").hide();
|
$("#clear-notify-logs").hide();
|
||||||
$("#clear-login-logs").hide();
|
$("#clear-login-logs").hide();
|
||||||
loadPlexPyLogs('plex_websocket', selected_log_level);
|
loadtautullilogs('plex_websocket', selected_log_level);
|
||||||
clearSearchButton('plex_websocket_log_table', log_table);
|
clearSearchButton('plex_websocket_log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#plex-logs-btn").click(function () {
|
$("#plex-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").hide();
|
$("#tautulli-log-levels").hide();
|
||||||
$("#plex-log-levels").show();
|
$("#plex-log-levels").show();
|
||||||
$("#clear-logs").hide();
|
$("#clear-logs").hide();
|
||||||
$("#download-plexpylog").hide()
|
$("#download-tautullilog").hide();
|
||||||
$("#download-plexserverlog").show()
|
$("#download-plexserverlog").show();
|
||||||
$("#download-plexscannerlog").hide()
|
$("#download-plexscannerlog").hide();
|
||||||
$("#clear-notify-logs").hide();
|
$("#clear-notify-logs").hide();
|
||||||
$("#clear-login-logs").hide();
|
$("#clear-login-logs").hide();
|
||||||
loadPlexLogs();
|
loadPlexLogs();
|
||||||
@@ -341,12 +341,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#plex-scanner-logs-btn").click(function () {
|
$("#plex-scanner-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").hide();
|
$("#tautulli-log-levels").hide();
|
||||||
$("#plex-log-levels").show();
|
$("#plex-log-levels").show();
|
||||||
$("#clear-logs").hide();
|
$("#clear-logs").hide();
|
||||||
$("#download-plexpylog").hide()
|
$("#download-tautullilog").hide();
|
||||||
$("#download-plexserverlog").hide()
|
$("#download-plexserverlog").hide();
|
||||||
$("#download-plexscannerlog").show()
|
$("#download-plexscannerlog").show();
|
||||||
$("#clear-notify-logs").hide();
|
$("#clear-notify-logs").hide();
|
||||||
$("#clear-login-logs").hide();
|
$("#clear-login-logs").hide();
|
||||||
loadPlexScannerLogs();
|
loadPlexScannerLogs();
|
||||||
@@ -354,12 +354,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#notification-logs-btn").click(function () {
|
$("#notification-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").hide();
|
$("#tautulli-log-levels").hide();
|
||||||
$("#plex-log-levels").hide();
|
$("#plex-log-levels").hide();
|
||||||
$("#clear-logs").hide();
|
$("#clear-logs").hide();
|
||||||
$("#download-plexpylog").hide()
|
$("#download-tautullilog").hide();
|
||||||
$("#download-plexserverlog").hide()
|
$("#download-plexserverlog").hide();
|
||||||
$("#download-plexscannerlog").hide()
|
$("#download-plexscannerlog").hide();
|
||||||
$("#clear-notify-logs").show();
|
$("#clear-notify-logs").show();
|
||||||
$("#clear-login-logs").hide();
|
$("#clear-login-logs").hide();
|
||||||
loadNotificationLogs();
|
loadNotificationLogs();
|
||||||
@@ -367,12 +367,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#login-logs-btn").click(function () {
|
$("#login-logs-btn").click(function () {
|
||||||
$("#plexpy-log-levels").hide();
|
$("#tautulli-log-levels").hide();
|
||||||
$("#plex-log-levels").hide();
|
$("#plex-log-levels").hide();
|
||||||
$("#clear-logs").hide();
|
$("#clear-logs").hide();
|
||||||
$("#download-plexpylog").hide()
|
$("#download-tautullilog").hide();
|
||||||
$("#download-plexserverlog").hide()
|
$("#download-plexserverlog").hide();
|
||||||
$("#download-plexscannerlog").hide()
|
$("#download-plexscannerlog").hide();
|
||||||
$("#clear-notify-logs").hide();
|
$("#clear-notify-logs").hide();
|
||||||
$("#clear-login-logs").show();
|
$("#clear-login-logs").show();
|
||||||
loadLoginLogs();
|
loadLoginLogs();
|
||||||
@@ -384,9 +384,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-logs").click(function () {
|
$("#clear-logs").click(function () {
|
||||||
var logfile = $(".tab-pane.active").data('logfile')
|
var logfile = $(".tab-pane.active").data('logfile');
|
||||||
|
var title = $("#log_tabs li.active a").text();
|
||||||
|
|
||||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli logs?");
|
$("#confirm-message").text("Are you sure you want to clear the " + title + "?");
|
||||||
$('#confirm-modal').modal();
|
$('#confirm-modal').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -396,7 +397,7 @@
|
|||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
result = $.parseJSON(xhr.responseText);
|
result = $.parseJSON(xhr.responseText);
|
||||||
msg = result.message;
|
msg = result.message;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||||
} else {
|
} else {
|
||||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||||
@@ -407,7 +408,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#download-plexpylog").click(function () {
|
$("#download-tautullilog").click(function () {
|
||||||
var logfile = $(".tab-pane.active").data('logfile');
|
var logfile = $(".tab-pane.active").data('logfile');
|
||||||
window.location.href = "download_log?logfile=" + logfile;
|
window.location.href = "download_log?logfile=" + logfile;
|
||||||
});
|
});
|
||||||
@@ -421,7 +422,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-notify-logs").click(function () {
|
$("#clear-notify-logs").click(function () {
|
||||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli notification logs?");
|
$("#confirm-message").text("Are you sure you want to clear the Tautulli Notification Logs?");
|
||||||
$('#confirm-modal').modal();
|
$('#confirm-modal').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -430,7 +431,7 @@
|
|||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
result = $.parseJSON(xhr.responseText);
|
result = $.parseJSON(xhr.responseText);
|
||||||
msg = result.message;
|
msg = result.message;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||||
} else {
|
} else {
|
||||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||||
@@ -442,7 +443,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-login-logs").click(function () {
|
$("#clear-login-logs").click(function () {
|
||||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli login logs?");
|
$("#confirm-message").text("Are you sure you want to clear the Tautulli Login Logs?");
|
||||||
$('#confirm-modal').modal();
|
$('#confirm-modal').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -451,7 +452,7 @@
|
|||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
result = $.parseJSON(xhr.responseText);
|
result = $.parseJSON(xhr.responseText);
|
||||||
msg = result.message;
|
msg = result.message;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||||
} else {
|
} else {
|
||||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||||
@@ -472,10 +473,10 @@
|
|||||||
{
|
{
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
}
|
}
|
||||||
if(refreshrate.value != 0)
|
if(refreshrate.value !== 0)
|
||||||
{
|
{
|
||||||
timer = setInterval(function() {
|
timer = setInterval(function() {
|
||||||
if ($("#tabs-plexpy_log").hasClass("active") || $("#tabs-plexpy_api_log").hasClass("active") || $("#tabs-plex_websocket_log").hasClass("active")) {
|
if ($("#tabs-tautulli_log").hasClass("active") || $("#tabs-tautulli_api_log").hasClass("active") || $("#tabs-plex_websocket_log").hasClass("active")) {
|
||||||
log_table.ajax.reload();
|
log_table.ajax.reload();
|
||||||
} else if ($("#tabs-plex_log").hasClass("active")) {
|
} else if ($("#tabs-plex_log").hasClass("active")) {
|
||||||
plex_log_table.ajax.reload();
|
plex_log_table.ajax.reload();
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<%!
|
<%!
|
||||||
from plexpy import helpers, notifiers
|
import json
|
||||||
|
from plexpy import helpers, notifiers, users
|
||||||
available_notification_actions = notifiers.available_notification_actions()
|
available_notification_actions = notifiers.available_notification_actions()
|
||||||
|
|
||||||
|
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
|
||||||
|
sorted(user_emails, key=lambda u: u['user'])
|
||||||
%>
|
%>
|
||||||
% if notifier:
|
% if notifier:
|
||||||
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
|
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
|
||||||
@@ -39,7 +43,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${item['name']}">${item['label']}</label>
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
|
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
|
||||||
% if item['name'] == 'osx_notify_app':
|
% if item['name'] == 'osx_notify_app':
|
||||||
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
|
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
|
||||||
@@ -62,7 +66,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${item['name']}">${item['label']}</label>
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +84,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${item['name']}">${item['label']}</label>
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||||
% for key, value in sorted(item['select_options'].iteritems()):
|
% for key, value in sorted(item['select_options'].iteritems()):
|
||||||
% if key == item['value']:
|
% if key == item['value']:
|
||||||
@@ -94,6 +98,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="help-block">${item['description'] | n}</p>
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
</div>
|
</div>
|
||||||
|
% elif item['input_type'] == 'selectize':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||||
|
<option value="select-all">Select All</option>
|
||||||
|
<option value="remove-all">Remove All</option>
|
||||||
|
% if isinstance(item['select_options'], dict):
|
||||||
|
% for section, options in item['select_options'].iteritems():
|
||||||
|
<optgroup label="${section}">
|
||||||
|
% for option in sorted(options, key=lambda x: x['text'].lower()):
|
||||||
|
<option value="${option['value']}">${option['text']}</option>
|
||||||
|
% endfor
|
||||||
|
</optgroup>
|
||||||
|
% endfor
|
||||||
|
% else:
|
||||||
|
<option value="border-all"></option>
|
||||||
|
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
|
||||||
|
<option value="${option['value']}">${option['text']}</option>
|
||||||
|
% endfor
|
||||||
|
% endif
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +132,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="friendly_name">Description</label>
|
<label for="friendly_name">Description</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${notifier['friendly_name']}" size="30">
|
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${notifier['friendly_name']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,21 +163,19 @@
|
|||||||
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
|
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
|
||||||
<label>Notification Conditions</label>
|
<label>Notification Conditions</label>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Add custom notification conditions.
|
Add custom conditions to only <em>allow certain notifications</em>. By default, all notifications will be sent if there are no conditions.
|
||||||
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block">
|
|
||||||
Note: Conditions are checked after the notification trigger and the notification will only be sent if the condition logic is satisfied.
|
|
||||||
</p>
|
|
||||||
<div id="condition-widget"></div>
|
<div id="condition-widget"></div>
|
||||||
<input type="hidden" name="custom_conditions" id="custom_conditions" />
|
<input type="hidden" name="custom_conditions" id="custom_conditions" />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="custom_conditions_logic">Condition Logic</label>
|
<label for="custom_conditions_logic">Condition Logic</label>
|
||||||
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" required />
|
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" />
|
||||||
<div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div>
|
<div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Enter the logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
|
Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
|
||||||
|
Leave blank for implicit <span class="inline-pre">and</span> between all conditions.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Note: Only the keywords <span class="inline-pre">and</span>/<span class="inline-pre">or</span> and brackets <span class="inline-pre">()</span> are supported.
|
Note: Only the keywords <span class="inline-pre">and</span>/<span class="inline-pre">or</span> and brackets <span class="inline-pre">()</span> are supported.
|
||||||
@@ -187,7 +216,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<input type="button" class="btn btn-bright notifier-text-preview" data-action="${action['name']}" value="Preview Arguments">
|
<input type="button" class="btn btn-bright notifier-text-preview" data-action="${action['name']}" value="Preview Arguments">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +243,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<input type="button" class="btn btn-bright notifier-text-preview" data-action="${action['name']}" value="Preview Text">
|
<input type="button" class="btn btn-bright notifier-text-preview" data-action="${action['name']}" value="Preview Text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,7 +309,7 @@
|
|||||||
% endif
|
% endif
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<input type="button" class="btn btn-bright" id="test_notifier" name="test_notifier" value="Test ${notifier['agent_label']}">
|
<input type="button" class="btn btn-bright" id="test_notifier" name="test_notifier" value="Test ${notifier['agent_label']}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,16 +333,31 @@
|
|||||||
$('#notifier-config-modal').unbind('hidden.bs.modal');
|
$('#notifier-config-modal').unbind('hidden.bs.modal');
|
||||||
|
|
||||||
// Need this for setting conditions since conditions contain the character "
|
// Need this for setting conditions since conditions contain the character "
|
||||||
$('#custom_conditions').val('${notifier['custom_conditions'] | n}')
|
$('#custom_conditions').val(JSON.stringify(${json.dumps(notifier["custom_conditions"]) | n}));
|
||||||
|
|
||||||
$('#condition-widget').filterer({
|
$('#condition-widget').filterer({
|
||||||
parameters: ${parameters | n},
|
parameters: ${json.dumps(parameters) | n},
|
||||||
conditions: ${notifier["custom_conditions"] | n},
|
conditions: ${json.dumps(notifier["custom_conditions"]) | n},
|
||||||
updateConditions: function(newConditions){
|
updateConditions: function(newConditions){
|
||||||
$('#custom_conditions').val(JSON.stringify(newConditions));
|
$('#custom_conditions').val(JSON.stringify(newConditions));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
function setNegativeOperator(select) {
|
||||||
|
if (select.val() === 'does not contain' || select.val() === 'is not') {
|
||||||
|
select.closest('.form-group').find('.react-selectize-search-field-and-selected-values').addClass('negative-operator');
|
||||||
|
} else {
|
||||||
|
select.closest('.form-group').find('.react-selectize-search-field-and-selected-values').removeClass('negative-operator');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#condition-widget select[name=operator]').each(function () {
|
||||||
|
setNegativeOperator($(this));
|
||||||
|
});
|
||||||
|
$('#condition-widget').on('change', 'select[name=operator]', function () {
|
||||||
|
setNegativeOperator($(this));
|
||||||
|
});
|
||||||
|
|
||||||
function reloadModal() {
|
function reloadModal() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_notifier_config_modal',
|
url: 'get_notifier_config_modal',
|
||||||
@@ -330,7 +374,7 @@
|
|||||||
if (jqXHR) {
|
if (jqXHR) {
|
||||||
var result = $.parseJSON(jqXHR.responseText);
|
var result = $.parseJSON(jqXHR.responseText);
|
||||||
var msg = result.message;
|
var msg = result.message;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||||
} else {
|
} else {
|
||||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||||
@@ -390,7 +434,7 @@
|
|||||||
|
|
||||||
% if notifier['agent_name'] == 'facebook':
|
% if notifier['agent_name'] == 'facebook':
|
||||||
function disableFacebookRequest() {
|
function disableFacebookRequest() {
|
||||||
if ($('#facebook_app_id').val() != '' && $('#facebook_app_secret').val() != '') { $('#facebook_facebookStep1').prop('disabled', false); }
|
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebookStep1').prop('disabled', false); }
|
||||||
else { $('#facebook_facebookStep1').prop('disabled', true); }
|
else { $('#facebook_facebookStep1').prop('disabled', true); }
|
||||||
}
|
}
|
||||||
disableFacebookRequest();
|
disableFacebookRequest();
|
||||||
@@ -404,19 +448,20 @@
|
|||||||
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
|
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var facebook_token;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'facebookStep1',
|
url: 'facebookStep1',
|
||||||
data: {
|
data: {
|
||||||
app_id: $('#facebook_app_id').val(),
|
app_id: $('#facebook_app_id').val(),
|
||||||
app_secret: $('#facebook_app_secret').val(),
|
app_secret: $('#facebook_app_secret').val(),
|
||||||
redirect_uri: $('#facebook_redirect_uri').val(),
|
redirect_uri: $('#facebook_redirect_uri').val()
|
||||||
},
|
},
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
var result = $.parseJSON(xhr.responseText);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
var msg = result.msg;
|
var msg = result.msg;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||||
window.open(result.url);
|
window.open(result.url);
|
||||||
|
|
||||||
@@ -455,18 +500,128 @@
|
|||||||
|
|
||||||
$('#notifier-config-modal').on('hidden.bs.modal', function () {
|
$('#notifier-config-modal').on('hidden.bs.modal', function () {
|
||||||
facebook_token = false;
|
facebook_token = false;
|
||||||
})
|
});
|
||||||
|
|
||||||
% elif notifier['agent_name'] == 'browser':
|
% elif notifier['agent_name'] == 'browser':
|
||||||
$('#browser_allow_browser').click(function () {
|
$('#browser_allow_browser').click(function () {
|
||||||
PNotify.desktop.permission();
|
PNotify.desktop.permission();
|
||||||
})
|
});
|
||||||
|
|
||||||
% elif notifier['agent_name'] == 'osx':
|
% elif notifier['agent_name'] == 'osx':
|
||||||
$('#osxnotifyregister').click(function () {
|
$('#osxnotifyregister').click(function () {
|
||||||
var osx_notify_app = $('#osx_notify_app').val();
|
var osx_notify_app = $('#osx_notify_app').val();
|
||||||
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||||
})
|
});
|
||||||
|
|
||||||
|
% elif notifier['agent_name'] == 'email':
|
||||||
|
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
|
||||||
|
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)';
|
||||||
|
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
|
||||||
|
plugins: ['remove_button'],
|
||||||
|
maxItems: null,
|
||||||
|
render: {
|
||||||
|
item: function(item, escape) {
|
||||||
|
return '<div>' +
|
||||||
|
(item.text ? '<span class="item-text">' + escape(item.text) + '</span>' : '') +
|
||||||
|
(item.value ? '<span class="item-value">' + escape(item.value) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
},
|
||||||
|
option: function(item, escape) {
|
||||||
|
var label = item.text || item.value;
|
||||||
|
var caption = item.text ? item.value : null;
|
||||||
|
if (item.value.endsWith('-all')) {
|
||||||
|
return '<div class="' + item.value + '">' + escape(label) + '</div>'
|
||||||
|
}
|
||||||
|
return '<div>' +
|
||||||
|
escape(label) +
|
||||||
|
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemAdd: function(value) {
|
||||||
|
if (value === 'select-all') {
|
||||||
|
var all_keys = $.map(this.options, function(option){
|
||||||
|
return option.value.endsWith('-all') ? null : option.value;
|
||||||
|
});
|
||||||
|
this.setValue(all_keys);
|
||||||
|
} else if (value === 'remove-all') {
|
||||||
|
this.clear();
|
||||||
|
this.refreshOptions();
|
||||||
|
this.positionDropdown();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createFilter: function(input) {
|
||||||
|
var match, regex;
|
||||||
|
|
||||||
|
// email@address.com
|
||||||
|
regex = new RegExp('^' + REGEX_EMAIL + '$', 'i');
|
||||||
|
match = input.match(regex);
|
||||||
|
if (match) return !this.options.hasOwnProperty(match[0]);
|
||||||
|
|
||||||
|
// user <email@address.com>
|
||||||
|
regex = new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i');
|
||||||
|
match = input.match(regex);
|
||||||
|
if (match) return !this.options.hasOwnProperty(match[2]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
create: function(input) {
|
||||||
|
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
|
||||||
|
return {value: input};
|
||||||
|
}
|
||||||
|
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
value : match[2],
|
||||||
|
text : $.trim(match[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var email_to = $email_selectors[0].selectize;
|
||||||
|
var email_cc = $email_selectors[1].selectize;
|
||||||
|
var email_bcc = $email_selectors[2].selectize;
|
||||||
|
email_to.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'email_to'), [])) | n});
|
||||||
|
email_cc.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'email_cc'), [])) | n});
|
||||||
|
email_bcc.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'email_bcc'), [])) | n});
|
||||||
|
|
||||||
|
% elif notifier['agent_name'] == 'join':
|
||||||
|
var $join_device_names = $('#join_device_names').selectize({
|
||||||
|
plugins: ['remove_button'],
|
||||||
|
maxItems: null,
|
||||||
|
create: true
|
||||||
|
});
|
||||||
|
var join_device_names = $join_device_names[0].selectize;
|
||||||
|
join_device_names.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
|
||||||
|
|
||||||
|
% elif notifier['agent_name'] == 'zapier':
|
||||||
|
$('#zapier_test_hook').click(function () {
|
||||||
|
$.get('zapier_test_hook', { 'zapier_hook': $('#zapier_hook').val() }, function (data) {
|
||||||
|
if (data.result === 'success') {
|
||||||
|
showMsg('<i class="fa fa-check"></i> ' + data.msg, false, true, 5000);
|
||||||
|
} else {
|
||||||
|
showMsg('<i class="fa fa-times"></i> ' + data.msg, false, true, 5000, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
% elif notifier['agent_name'] == 'pushover':
|
||||||
|
function pushoverPriority() {
|
||||||
|
if ($('#pushover_priority').val() == '2') {
|
||||||
|
$('#pushover_retry').closest('.form-group').show();
|
||||||
|
$('#pushover_expire').closest('.form-group').show();
|
||||||
|
} else {
|
||||||
|
$('#pushover_retry').closest('.form-group').hide();
|
||||||
|
$('#pushover_expire').closest('.form-group').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushoverPriority();
|
||||||
|
$('#pushover_priority').change( function () {
|
||||||
|
pushoverPriority();
|
||||||
|
});
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
function validateLogic() {
|
function validateLogic() {
|
||||||
@@ -597,7 +752,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function sendTestNotification() {
|
function sendTestNotification() {
|
||||||
if ('${notifier["agent_name"]}' != 'browser') {
|
if ('${notifier["agent_name"]}' !== 'browser') {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'send_notification',
|
url: 'send_notification',
|
||||||
data: {
|
data: {
|
||||||
@@ -621,7 +776,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if ($('#browser_auto_hide_delay').val() == "0") {
|
if ($('#browser_auto_hide_delay').val() === "0") {
|
||||||
PNotify.prototype.options.hide = false;
|
PNotify.prototype.options.hide = false;
|
||||||
} else {
|
} else {
|
||||||
PNotify.prototype.options.hide = true;
|
PNotify.prototype.options.hide = true;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ DOCUMENTATION :: END
|
|||||||
<h3 class="text-muted"> </h3>
|
<h3 class="text-muted"> </h3>
|
||||||
</div>
|
</div>
|
||||||
% elif item['media_type'] == 'show':
|
% elif item['media_type'] == 'show':
|
||||||
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}">
|
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||||
<div class="dashboard-recent-media-poster">
|
<div class="dashboard-recent-media-poster">
|
||||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||||
<div class="dashboard-recent-media-overlay">
|
<div class="dashboard-recent-media-overlay">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ DOCUMENTATION :: END
|
|||||||
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
|
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
|
||||||
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% elif job in ('Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
|
% elif job in ('Check for server response', 'Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
|
||||||
<tr>
|
<tr>
|
||||||
<td>${job}</td>
|
<td>${job}</td>
|
||||||
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
|
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -41,13 +41,16 @@
|
|||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
<script>
|
<script>
|
||||||
|
// Remove the update bar
|
||||||
|
$('#updatebar').remove();
|
||||||
|
|
||||||
// Use p.countdown as container, pass redirect, duration, and optional message
|
// Use p.countdown as container, pass redirect, duration, and optional message
|
||||||
$(".countdown").countdown(reloadPage, ${timer}, "");
|
$(".countdown").countdown(reloadPage, ${timer}, "");
|
||||||
$('#state-change-modal').modal({
|
|
||||||
keyboard: false
|
|
||||||
})
|
|
||||||
// Make modal visible
|
// Make modal visible
|
||||||
$('#state-change-modal').modal('show')
|
$('#state-change-modal').modal({
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false
|
||||||
|
}).show();
|
||||||
|
|
||||||
// Redirect to home page after countdown.
|
// Redirect to home page after countdown.
|
||||||
function reloadPage() {
|
function reloadPage() {
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ DOCUMENTATION :: END
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
% if data['current_session']:
|
||||||
|
<div class="col-sm-12 text-muted stream-info-current">
|
||||||
|
<i class="fa fa-exclamation-circle"></i> Current session. Updated stream details below may be delayed.
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<table class="stream-info" style="margin-top: 0;">
|
<table class="stream-info" style="margin-top: 0;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<style>
|
<style>
|
||||||
td {word-wrap: break-word}
|
td {word-wrap: break-word}
|
||||||
@@ -20,13 +20,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select syncs to delete. Data is deleted upon exiting edit mode.</div>
|
<div class="alert alert-danger alert-edit" role="alert" id="sync-row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select syncs to delete. Data is deleted upon exiting delete mode.</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="sync-row-edit-mode">
|
||||||
<i class="fa fa-pencil"></i> Edit mode
|
<i class="fa fa-trash-o"></i> Delete mode
|
||||||
</button> 
|
</button> 
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
|
<div class="btn-group" id="user-selection">
|
||||||
|
<label>
|
||||||
|
<select name="sync-user" id="sync-user" class="btn" style="color: inherit;">
|
||||||
|
<option value="">All Users</option>
|
||||||
|
<option disabled>────────────</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
|
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='table-card-back'>
|
<div class='table-card-back'>
|
||||||
<table class="display" id="sync_table" width="100%">
|
<table class="display sync_table" id="sync_table" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left" id="delete_row">Delete</th>
|
<th align="left" id="delete_row">Delete</th>
|
||||||
@@ -87,21 +97,49 @@
|
|||||||
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
|
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
sync_table_options.ajax = {
|
// Load user ids and names (for the selector)
|
||||||
url: 'get_sync',
|
$.ajax({
|
||||||
data: function (d) {
|
url: 'get_user_names',
|
||||||
d.user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
type: 'get',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (data) {
|
||||||
|
var select = $('#sync-user');
|
||||||
|
data.sort(function (a, b) {
|
||||||
|
return a.friendly_name.localeCompare(b.friendly_name);
|
||||||
|
});
|
||||||
|
data.forEach(function (item) {
|
||||||
|
select.append('<option value="' + item.user_id + '">' +
|
||||||
|
item.friendly_name + '</option>');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
|
||||||
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0] } );
|
|
||||||
$( colvis.button() ).appendTo('div.colvis-button-bar');
|
|
||||||
|
|
||||||
clearSearchButton('sync_table', sync_table);
|
function loadSyncTable(selected_user_id) {
|
||||||
|
sync_table_options.ajax = {
|
||||||
|
url: 'get_sync?user_id=' + selected_user_id
|
||||||
|
};
|
||||||
|
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||||
|
var colvis = new $.fn.dataTable.ColVis(sync_table, {
|
||||||
|
buttonText: '<i class="fa fa-columns"></i> Select columns',
|
||||||
|
buttonClass: 'btn btn-dark',
|
||||||
|
exclude: [0]
|
||||||
|
});
|
||||||
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
|
clearSearchButton('sync_table', sync_table);
|
||||||
|
|
||||||
|
$('#sync-user').on('change', function () {
|
||||||
|
selected_user_id = $(this).val() || null;
|
||||||
|
sync_table.ajax.url('get_sync?user_id=' + selected_user_id).load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
|
||||||
|
loadSyncTable(selected_user_id);
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('#row-edit-mode').on('click', function() {
|
$('#sync-row-edit-mode').on('click', function() {
|
||||||
$('#row-edit-mode-alert').fadeIn(200);
|
$('#sync-row-edit-mode-alert').fadeIn(200);
|
||||||
|
|
||||||
if ($(this).hasClass('active')) {
|
if ($(this).hasClass('active')) {
|
||||||
if (syncs_to_delete.length > 0) {
|
if (syncs_to_delete.length > 0) {
|
||||||
@@ -123,13 +161,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
sync_table.draw();
|
sync_table.ajax.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.delete-control').each(function () {
|
$('.delete-control').each(function () {
|
||||||
$(this).addClass('hidden');
|
$(this).addClass('hidden');
|
||||||
$('#row-edit-mode-alert').fadeOut(200);
|
$('#sync-row-edit-mode-alert').fadeOut(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -144,7 +182,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#refresh-syncs-list").click(function() {
|
$("#refresh-syncs-list").click(function() {
|
||||||
sync_table.draw();
|
sync_table.ajax.reload();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ DOCUMENTATION :: END
|
|||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -213,13 +213,25 @@ DOCUMENTATION :: END
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
|
<div class="alert alert-danger alert-edit" role="alert" id="sync-row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select syncs to delete. Data is deleted upon exiting delete mode.</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="sync-row-edit-mode">
|
||||||
|
<i class="fa fa-trash-o"></i> Delete mode
|
||||||
|
</button> 
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
|
||||||
|
</div>
|
||||||
<div class="btn-group colvis-button-bar" id="button-bar-sync"></div>
|
<div class="btn-group colvis-button-bar" id="button-bar-sync"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<table class="display" id="sync_table-UID-${data['user_id']}" width="100%">
|
<table class="display sync_table" id="sync_table-UID-${data['user_id']}" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th align="left" id="delete_row">Delete</th>
|
||||||
<th align="left" id="state">State</th>
|
<th align="left" id="state">State</th>
|
||||||
<th align="left" id="username">Username</th>
|
<th align="left" id="username">Username</th>
|
||||||
<th align="left" id="sync_title">Title</th>
|
<th align="left" id="sync_title">Title</th>
|
||||||
@@ -252,6 +264,11 @@ DOCUMENTATION :: END
|
|||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="button-bar">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-dark refresh-ip-address-button" id="refresh-ip-address-list"><i class="fa fa-refresh"></i> Refresh IP addresses</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
||||||
@@ -284,6 +301,9 @@ DOCUMENTATION :: END
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-dark refresh-login-button" id="refresh-login-list"><i class="fa fa-refresh"></i> Refresh logins</button>
|
||||||
|
</div>
|
||||||
<div class="btn-group colvis-button-bar" id="button-bar-login"></div>
|
<div class="btn-group colvis-button-bar" id="button-bar-login"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -298,6 +318,7 @@ DOCUMENTATION :: END
|
|||||||
<th align="left" id="host">Host</th>
|
<th align="left" id="host">Host</th>
|
||||||
<th align="left" id="os">Operating System</th>
|
<th align="left" id="os">Operating System</th>
|
||||||
<th align="left" id="browser">Browser</th>
|
<th align="left" id="browser">Browser</th>
|
||||||
|
<th align="left" id="login_success"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
@@ -351,7 +372,7 @@ DOCUMENTATION :: END
|
|||||||
<h4 class="modal-title" id="myModalLabel">Confirm Delete</h4>
|
<h4 class="modal-title" id="myModalLabel">Confirm Delete</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="text-align: center;">
|
<div class="modal-body" style="text-align: center;">
|
||||||
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
|
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> <span id="deleteType"></span> item(s)?</p>
|
||||||
<p>This is permanent and cannot be undone!</p>
|
<p>This is permanent and cannot be undone!</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -388,11 +409,6 @@ DOCUMENTATION :: END
|
|||||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('a[href="#tabs-profile"]').on('shown.bs.tab', function() {
|
|
||||||
var media_type = null;
|
|
||||||
loadHistoryTable(media_type);
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadHistoryTable(media_type) {
|
function loadHistoryTable(media_type) {
|
||||||
// Build watch history table
|
// Build watch history table
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
@@ -405,7 +421,7 @@ DOCUMENTATION :: END
|
|||||||
media_type: media_type
|
media_type: media_type
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
|
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
|
||||||
history_table.column(2).visible(false);
|
history_table.column(2).visible(false);
|
||||||
|
|
||||||
@@ -423,29 +439,21 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
function loadSyncTable() {
|
||||||
var media_type = null;
|
|
||||||
loadHistoryTable(media_type);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
|
|
||||||
// Build user sync table
|
// Build user sync table
|
||||||
sync_table_options.ajax = {
|
sync_table_options.ajax = {
|
||||||
url: 'get_sync',
|
url: 'get_sync?user_id=' + user_id
|
||||||
data: function(d) {
|
};
|
||||||
d.user_id = user_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
|
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
|
||||||
sync_table.column(1).visible(false);
|
sync_table.column(2).visible(false);
|
||||||
|
|
||||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0] } );
|
||||||
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
||||||
|
|
||||||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
||||||
});
|
}
|
||||||
|
|
||||||
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
|
function loadIPAddressTable() {
|
||||||
// Build user IP table
|
// Build user IP table
|
||||||
user_ip_table_options.ajax = {
|
user_ip_table_options.ajax = {
|
||||||
url: 'get_user_ips',
|
url: 'get_user_ips',
|
||||||
@@ -456,27 +464,71 @@ DOCUMENTATION :: END
|
|||||||
user_id: user_id
|
user_id: user_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
|
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
|
||||||
|
|
||||||
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
|
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
|
||||||
});
|
}
|
||||||
|
|
||||||
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
|
function loadLoginTable() {
|
||||||
// Build user login table
|
// Build user login table
|
||||||
login_log_table_options.ajax = {
|
login_log_table_options.ajax = {
|
||||||
url: 'get_user_logins',
|
url: 'get_user_logins',
|
||||||
data: function(d) {
|
data: function(d) {
|
||||||
d.user_id = user_id;
|
return {
|
||||||
|
json_data: JSON.stringify(d),
|
||||||
|
user_id: user_id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
|
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
|
||||||
login_log_table.columns([1, 2]).visible(false);
|
login_log_table.columns([1, 2]).visible(false);
|
||||||
|
|
||||||
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [7] } );
|
||||||
$( colvis_login.button() ).appendTo('#button-bar-login');
|
$( colvis_login.button() ).appendTo('#button-bar-login');
|
||||||
|
|
||||||
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
||||||
|
if (typeof(history_table) === 'undefined') {
|
||||||
|
var media_type = null;
|
||||||
|
loadHistoryTable(media_type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
|
||||||
|
if (typeof(sync_table) === 'undefined') {
|
||||||
|
loadSyncTable(user_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
|
||||||
|
if (typeof(user_ip_table) === 'undefined') {
|
||||||
|
loadIPAddressTable(user_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
|
||||||
|
if (typeof(login_log_table) === 'undefined') {
|
||||||
|
loadLoginTable(user_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-history-list").click(function () {
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-syncs-list").click(function() {
|
||||||
|
sync_table.ajax.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-ip-address-list").click(function () {
|
||||||
|
user_ip_table.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-login-list").click(function () {
|
||||||
|
login_log_table.draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
@@ -502,6 +554,7 @@ DOCUMENTATION :: END
|
|||||||
if ($(this).hasClass('active')) {
|
if ($(this).hasClass('active')) {
|
||||||
if (history_to_delete.length > 0) {
|
if (history_to_delete.length > 0) {
|
||||||
$('#deleteCount').text(history_to_delete.length);
|
$('#deleteCount').text(history_to_delete.length);
|
||||||
|
$('#deleteType').text('history');
|
||||||
$('#confirm-modal-delete').modal();
|
$('#confirm-modal-delete').modal();
|
||||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||||
history_to_delete.forEach(function(row, idx) {
|
history_to_delete.forEach(function(row, idx) {
|
||||||
@@ -520,14 +573,56 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.delete-control').each(function () {
|
$('.history_table .delete-control').each(function () {
|
||||||
$(this).addClass('hidden');
|
$(this).addClass('hidden');
|
||||||
$('#row-edit-mode-alert').fadeOut(200);
|
$('#row-edit-mode-alert').fadeOut(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
history_to_delete = [];
|
history_to_delete = [];
|
||||||
$('.delete-control').each(function() {
|
$('.history_table .delete-control').each(function() {
|
||||||
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#sync-row-edit-mode').on('click', function() {
|
||||||
|
$('#sync-row-edit-mode-alert').fadeIn(200);
|
||||||
|
|
||||||
|
if ($(this).hasClass('active')) {
|
||||||
|
if (syncs_to_delete.length > 0) {
|
||||||
|
$('#deleteCount').text(syncs_to_delete.length);
|
||||||
|
$('#deleteType').text('sync');
|
||||||
|
$('#confirm-modal-delete').modal();
|
||||||
|
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||||
|
syncs_to_delete.forEach(function(row, idx) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_sync_rows',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
client_id: row.client_id,
|
||||||
|
sync_id: row.sync_id
|
||||||
|
},
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "Sync deleted";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
sync_table.ajax.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.sync_table .delete-control').each(function () {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
$('#sync-row-edit-mode-alert').fadeOut(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
syncs_to_delete = [];
|
||||||
|
$('.sync_table .delete-control').each(function() {
|
||||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
$(this).removeClass('hidden');
|
$(this).removeClass('hidden');
|
||||||
});
|
});
|
||||||
@@ -535,10 +630,6 @@ DOCUMENTATION :: END
|
|||||||
});
|
});
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
$("#refresh-history-list").click(function () {
|
|
||||||
history_table.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
function recentlyWatched() {
|
function recentlyWatched() {
|
||||||
// Populate recently watched
|
// Populate recently watched
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
|
||||||
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
|
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
@@ -184,18 +184,20 @@
|
|||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$("#refresh-users-list").click(function() {
|
$("#refresh-users-list").click(function() {
|
||||||
|
showMsg('Refreshing users list...', true, false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'refresh_users_list',
|
url: 'refresh_users_list',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success: function(data) {
|
complete: function (xhr, status) {
|
||||||
showMsg('<i class="fa fa-check"></i> Users list refresh started...', false, true, 2000, false);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
},
|
var msg = result.message;
|
||||||
complete: function (data) {
|
if (result.result == 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> Users list refreshed.', false, true, 2000, false);
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||||
},
|
users_list_table.draw();
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
} else {
|
||||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh users list.', false, true, 2000, true);
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<%
|
<%
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import common
|
from plexpy import common, helpers
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/bootstrap-wizard.css" rel="stylesheet">
|
<link href="${http_root}css/bootstrap-wizard.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
|
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||||
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet">
|
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||||
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="wizard" id="some-wizard" data-title="Tautulli Setup Wizard">
|
<div class="wizard" id="setup-wizard" data-title="Tautulli Setup Wizard">
|
||||||
<form>
|
<form>
|
||||||
<div class="wizard-card" data-cardname="card1">
|
<div class="wizard-card" data-cardname="card1">
|
||||||
<div style="float: right;">
|
<div style="float: right;">
|
||||||
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object>
|
<img src="${http_root}images/logo-tautulli-45.png" height="45" alt="PlexPy">
|
||||||
</div>
|
</div>
|
||||||
<h3 style="line-height: 50px;">Welcome!</h3>
|
<h3 style="line-height: 50px;">Welcome!</h3>
|
||||||
<br />
|
<br />
|
||||||
@@ -82,44 +82,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken">
|
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="" data-validate="validatePMStoken">
|
||||||
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-card" data-cardname="card3">
|
<div class="wizard-card" data-cardname="card3">
|
||||||
<h3>Plex Media Server</h3>
|
<h3>Plex Media Server</h3>
|
||||||
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure Tautulli can reach the server.</p>
|
<p class="help-block">
|
||||||
|
Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.
|
||||||
|
</p>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<label for="pms_ip">Plex IP or Hostname</label>
|
<label for="pms_ip">Plex IP or Hostname</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-8">
|
<div class="col-xs-12">
|
||||||
<select id="pms_ip" name="pms_ip"></select>
|
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip" name="pms_ip">
|
||||||
|
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<label for="pms_port">Port Number</label>
|
<label for="pms_port">Plex Port</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-3">
|
<div class="col-xs-3">
|
||||||
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-4">
|
<div class="col-xs-4">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Use SSL
|
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
|
||||||
|
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-4">
|
<div class="col-xs-4">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1"> Remote Server
|
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
|
||||||
|
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
|
<input type="hidden" id="pms_valid" data-validate="validatePMSip" value="">
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
|
||||||
|
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||||
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -200,106 +207,6 @@
|
|||||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
|
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
|
|
||||||
$.fn.wizard.logging = false;
|
|
||||||
var options = {
|
|
||||||
keyboard : false,
|
|
||||||
contentHeight : 400,
|
|
||||||
contentWidth : 700,
|
|
||||||
backdrop: 'static',
|
|
||||||
buttons: {submitText: 'Finish'},
|
|
||||||
submitUrl: "configUpdate"
|
|
||||||
};
|
|
||||||
var wizard = $("#some-wizard").wizard(options);
|
|
||||||
wizard.show();
|
|
||||||
|
|
||||||
wizard.on("submit", function(wizard) {
|
|
||||||
// Probably should not success before we know, but hopefully validation is good enough.
|
|
||||||
wizard.submitSuccess();
|
|
||||||
$.ajax({
|
|
||||||
url: "configUpdate",
|
|
||||||
type: "POST",
|
|
||||||
url: wizard.args.submitUrl,
|
|
||||||
data: wizard.serialize(),
|
|
||||||
dataType: "json",
|
|
||||||
complete: function (data) {
|
|
||||||
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
$select_pms = $('#pms_ip').selectize({
|
|
||||||
create: true,
|
|
||||||
createOnBlur: true,
|
|
||||||
openOnFocus: true,
|
|
||||||
maxItems: 1,
|
|
||||||
closeAfterSelect: true,
|
|
||||||
onInitialize: function () {
|
|
||||||
var s = this;
|
|
||||||
this.revertSettings.$children.each(function () {
|
|
||||||
$.extend(s.options[this.value], $(this).data());
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
render: {
|
|
||||||
option: function (item, escape) {
|
|
||||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
|
||||||
},
|
|
||||||
item: function (item, escape) {
|
|
||||||
// first item is rendered before initialization bug?
|
|
||||||
if (!item.ci) {
|
|
||||||
$.extend(item,
|
|
||||||
$(this.revertSettings.$children)
|
|
||||||
.filter('[value="' + item.value + '"]').data());
|
|
||||||
|
|
||||||
}
|
|
||||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChange: function (item) {
|
|
||||||
var ci = $('.selectize-input').find('div').attr('data-ci');
|
|
||||||
var port = $('.selectize-input').find('div').attr('data-port')
|
|
||||||
var local = $('.selectize-input').find('div').attr('data-local')
|
|
||||||
var ssl = $('.selectize-input').find('div').attr('data-use_ssl')
|
|
||||||
|
|
||||||
$("#pms-verify-status").html("");
|
|
||||||
// If a option was added by a user its
|
|
||||||
// data-xxx="undefined"
|
|
||||||
if (ci != "undefined") {
|
|
||||||
// To allow next step in the guide.
|
|
||||||
// servers with clientIdentifier is verified
|
|
||||||
$("#pms_identifier").val(ci);
|
|
||||||
$("#pms_valid").val("valid");
|
|
||||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
|
|
||||||
} else {
|
|
||||||
// Self made options must be verified
|
|
||||||
$("#pms_valid").val("");
|
|
||||||
$("#pms-verify-status").html("").hide();
|
|
||||||
}
|
|
||||||
// If the server is verified set the correct port
|
|
||||||
if (port != "undefined") {
|
|
||||||
$('#pms_port').val(port);
|
|
||||||
} else {
|
|
||||||
// set default port
|
|
||||||
$('#pms_port').val("32400");
|
|
||||||
}
|
|
||||||
if (local != "undefined" && local == '0') {
|
|
||||||
$('#pms_is_remote').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
$('#pms_is_remote').prop('checked', false);
|
|
||||||
}
|
|
||||||
if (ssl != "undefined" && ssl == "1") {
|
|
||||||
$('#pms_ssl').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
$('#pms_ssl').prop('checked', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function validatePMSip(el) {
|
function validatePMSip(el) {
|
||||||
var valid_pms_ip = el.val();
|
var valid_pms_ip = el.val();
|
||||||
var retValue = {};
|
var retValue = {};
|
||||||
@@ -352,6 +259,146 @@
|
|||||||
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
|
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
$.fn.wizard.logging = false;
|
||||||
|
var options = {
|
||||||
|
keyboard : false,
|
||||||
|
contentHeight : 400,
|
||||||
|
contentWidth : 700,
|
||||||
|
backdrop: 'static',
|
||||||
|
buttons: {submitText: 'Finish'},
|
||||||
|
submitUrl: "configUpdate"
|
||||||
|
};
|
||||||
|
var wizard = $("#setup-wizard").wizard(options);
|
||||||
|
wizard.show();
|
||||||
|
|
||||||
|
// Change button classes
|
||||||
|
wizard.find('.wizard-back').addClass('btn-dark');
|
||||||
|
wizard.on('incrementCard', function(wizard) {
|
||||||
|
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
|
||||||
|
});
|
||||||
|
wizard.on('decrementCard', function(wizard) {
|
||||||
|
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
|
||||||
|
});
|
||||||
|
|
||||||
|
wizard.on("submit", function(wizard) {
|
||||||
|
// Probably should not success before we know, but hopefully validation is good enough.
|
||||||
|
wizard.submitSuccess();
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: wizard.args.submitUrl,
|
||||||
|
data: wizard.serialize(),
|
||||||
|
dataType: "json",
|
||||||
|
complete: function (data) {
|
||||||
|
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.checkbox-toggle').click(function () {
|
||||||
|
var configToggle = $(this).data('id');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$('#'+configToggle).val(1);
|
||||||
|
} else {
|
||||||
|
$('#'+configToggle).val(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var $select_pms = $('#pms_ip').selectize({
|
||||||
|
createOnBlur: true,
|
||||||
|
openOnFocus: true,
|
||||||
|
maxItems: 1,
|
||||||
|
closeAfterSelect: true,
|
||||||
|
sortField: 'label',
|
||||||
|
searchField: ['label', 'value'],
|
||||||
|
inputClass: 'form-control selectize-input',
|
||||||
|
render: {
|
||||||
|
item: function (item, escape) {
|
||||||
|
var label = item.label || item.value;
|
||||||
|
var caption = item.label ? item.value : null;
|
||||||
|
return '<div data-ssl="' + item.httpsRequired +
|
||||||
|
'" data-local="' + item.local +
|
||||||
|
'" data-identifier="' + item.clientIdentifier +
|
||||||
|
'" data-ip="' + item.ip +
|
||||||
|
'" data-port="' + item.port +
|
||||||
|
'" data-is_cloud="' + item.is_cloud +
|
||||||
|
'" data-label="' + item.label + '">' +
|
||||||
|
'<span class="item-text">' + escape(label) + '</span>' +
|
||||||
|
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
},
|
||||||
|
option: function (item, escape) {
|
||||||
|
var label = item.label || item.value;
|
||||||
|
var caption = item.label ? item.value : null;
|
||||||
|
return '<div data-ssl="' + item.httpsRequired +
|
||||||
|
'" data-local="' + item.local +
|
||||||
|
'" data-identifier="' + item.clientIdentifier +
|
||||||
|
'" data-ip="' + item.ip +
|
||||||
|
'" data-port="' + item.port +
|
||||||
|
'" data-is_cloud="' + item.is_cloud +
|
||||||
|
'" data-label="' + item.label + '">' +
|
||||||
|
escape(label) +
|
||||||
|
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: function(input) {
|
||||||
|
return {label: '', value: input};
|
||||||
|
},
|
||||||
|
onChange: function (item) {
|
||||||
|
var pms_ip_selected = this.getItem(item)[0];
|
||||||
|
var identifier = $(pms_ip_selected).data('identifier');
|
||||||
|
var port = $(pms_ip_selected).data('port');
|
||||||
|
var local = $(pms_ip_selected).data('local');
|
||||||
|
var ssl = $(pms_ip_selected).data('ssl');
|
||||||
|
var is_cloud = $(pms_ip_selected).data('is_cloud');
|
||||||
|
|
||||||
|
$("#pms_valid").val(identifier !== 'undefined' ? 'valid' : '');
|
||||||
|
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i> Server found!' : '').fadeIn('fast');
|
||||||
|
|
||||||
|
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||||
|
$('#pms_port').val(port !== 'undefined' ? port : 32400);
|
||||||
|
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||||
|
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||||
|
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||||
|
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||||
|
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
|
||||||
|
|
||||||
|
if (is_cloud === true) {
|
||||||
|
$('#pms_port').prop('readonly', true);
|
||||||
|
$('#pms_is_remote_checkbox').prop('disabled', true);
|
||||||
|
$('#pms_ssl_checkbox').prop('disabled', true);
|
||||||
|
} else {
|
||||||
|
$('#pms_port').prop('readonly', false);
|
||||||
|
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||||
|
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var select_pms = $select_pms[0].selectize;
|
||||||
|
|
||||||
|
function getServerOptions(token) {
|
||||||
|
/* Set token and returns server options */
|
||||||
|
$.ajax({
|
||||||
|
url: 'discover',
|
||||||
|
data: {
|
||||||
|
token: token
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result) {
|
||||||
|
var existing_value = $('#pms_ip').val();
|
||||||
|
result.forEach(function (item) {
|
||||||
|
if (item.value === existing_value) {
|
||||||
|
select_pms.updateOption(item.value, item);
|
||||||
|
} else {
|
||||||
|
select_pms.addOption(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
var pms_verified = false;
|
var pms_verified = false;
|
||||||
var authenticated = false;
|
var authenticated = false;
|
||||||
|
|
||||||
@@ -360,14 +407,20 @@
|
|||||||
var pms_ip = $("#pms_ip").val().trim();
|
var pms_ip = $("#pms_ip").val().trim();
|
||||||
var pms_port = $("#pms_port").val().trim();
|
var pms_port = $("#pms_port").val().trim();
|
||||||
var pms_identifier = $("#pms_identifier").val();
|
var pms_identifier = $("#pms_identifier").val();
|
||||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
var pms_ssl = $("#pms_ssl").val();
|
||||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
var pms_is_remote = $("#pms_is_remote").val();
|
||||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_server_id',
|
url: 'get_server_id',
|
||||||
data: { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
|
data: {
|
||||||
|
hostname: pms_ip,
|
||||||
|
port: pms_port,
|
||||||
|
identifier: pms_identifier,
|
||||||
|
ssl: pms_ssl,
|
||||||
|
remote: pms_is_remote
|
||||||
|
},
|
||||||
cache: true,
|
cache: true,
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -375,10 +428,11 @@
|
|||||||
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
|
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
},
|
},
|
||||||
success: function (json) {
|
success: function(xhr, status) {
|
||||||
var machine_identifier = json;
|
var result = xhr;
|
||||||
if (machine_identifier) {
|
var identifier = result.identifier;
|
||||||
$("#pms_identifier").val(machine_identifier);
|
if (identifier) {
|
||||||
|
$("#pms_identifier").val(identifier);
|
||||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
pms_verified = true;
|
pms_verified = true;
|
||||||
@@ -444,39 +498,7 @@
|
|||||||
$('#pms-token-status').fadeIn('fast');
|
$('#pms-token-status').fadeIn('fast');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// Send database path to import script
|
|
||||||
//$("#plexwatch-import").click(function() {
|
|
||||||
// var database_path = $("#db_location").val();
|
|
||||||
// var table_name = 'processed';
|
|
||||||
// var import_ignore_interval = 0;
|
|
||||||
// $.ajax({
|
|
||||||
// url: 'get_plexwatch_export_data',
|
|
||||||
// data: {database_path: database_path, table_name:table_name, import_ignore_interval:import_ignore_interval},
|
|
||||||
// cache: false,
|
|
||||||
// async: true,
|
|
||||||
// success: function(data) {
|
|
||||||
// if (data === 'Import has started. Check the Tautulli logs to monitor any problems.') {
|
|
||||||
// $("#plexwatch-import-status").html('Started');
|
|
||||||
// } else {
|
|
||||||
// $("#plexwatch-import-status").html(data);
|
|
||||||
// }
|
|
||||||
// $("#db_location").val('')
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//});
|
|
||||||
|
|
||||||
function getServerOptions(token) {
|
|
||||||
/* Set token and returns server options */
|
|
||||||
$.ajax({
|
|
||||||
url: "discover/" + token,
|
|
||||||
success: function (result) {
|
|
||||||
$('#pms_ip').html("");
|
|
||||||
// Add all servers to the "combobox"
|
|
||||||
$select_pms[0].selectize.addOption(result);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
# PROVIDE: plexpy
|
# PROVIDE: tautulli
|
||||||
# REQUIRE: plexpy
|
# REQUIRE: tautulli
|
||||||
# KEYWORD: shutdown
|
# KEYWORD: shutdown
|
||||||
#
|
#
|
||||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||||
# to enable this service:
|
# to enable this service:
|
||||||
#
|
#
|
||||||
# plexpy_enable (bool): Set to NO by default.
|
# tautulli_enable (bool): Set to NO by default.
|
||||||
# Set it to YES to enable it.
|
# Set it to YES to enable it.
|
||||||
# plexpy_user: The user account PlexPy daemon runs as what
|
# tautulli_user: The user account Tautulli daemon runs as what
|
||||||
# you want it to be. It uses 'plexpy' user by
|
# you want it to be. It uses 'tautulli' user by
|
||||||
# default. Do not sets it as empty or it will run
|
# default. Do not sets it as empty or it will run
|
||||||
# as root.
|
# as root.
|
||||||
# plexpy_dir: Directory where PlexPy lives.
|
# tautulli_dir: Directory where Tautulli lives.
|
||||||
# Default: /usr/local/plexpy
|
# Default: /usr/local/share/Tautulli
|
||||||
# plexpy_chdir: Change to this directory before running PlexPy.
|
# tautulli_chdir: Change to this directory before running Tautulli.
|
||||||
# Default is same as plexpy_dir.
|
# Default is same as tautulli_dir.
|
||||||
# plexpy_pid: The name of the pidfile to create.
|
# tautulli_pid: The name of the pidfile to create.
|
||||||
# Default is plexpy.pid in plexpy_dir.
|
# Default is tautulli.pid in tautulli_dir.
|
||||||
|
|
||||||
. /etc/rc.subr
|
. /etc/rc.subr
|
||||||
|
|
||||||
name="plexpy"
|
name="tautulli"
|
||||||
rcvar=${name}_enable
|
rcvar=${name}_enable
|
||||||
|
|
||||||
load_rc_config ${name}
|
load_rc_config ${name}
|
||||||
|
|
||||||
: ${plexpy_enable:="NO"}
|
: ${tautulli_enable:="NO"}
|
||||||
: ${plexpy_user:="plexpy"}
|
: ${tautulli_user:="tautulli"}
|
||||||
: ${plexpy_dir:="/usr/local/plexpy"}
|
: ${tautulli_dir:="/usr/local/share/Tautulli"}
|
||||||
: ${plexpy_chdir:="${plexpy_dir}"}
|
: ${tautulli_chdir:="${tautulli_dir}"}
|
||||||
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
|
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
|
||||||
: ${plexpy_conf:="${plexpy_dir}/config.ini"}
|
: ${tautulli_conf:="${tautulli_dir}/config.ini"}
|
||||||
|
|
||||||
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown PlexPy.
|
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown Tautulli.
|
||||||
if [ -e "${plexpy_conf}" ]; then
|
if [ -e "${tautulli_conf}" ]; then
|
||||||
HOST=`grep -A64 "\[General\]" "${plexpy_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/'`
|
HOST=`grep -A64 "\[General\]" "${tautulli_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/'`
|
||||||
PORT=`grep -A64 "\[General\]" "${plexpy_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/'`
|
PORT=`grep -A64 "\[General\]" "${tautulli_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/'`
|
||||||
fi
|
fi
|
||||||
|
|
||||||
status_cmd="${name}_status"
|
status_cmd="${name}_status"
|
||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
command="${plexpy_dir}/PlexPy.py"
|
command="${tautulli_dir}/Tautulli.py"
|
||||||
command_args="--daemon --quiet --nolaunch --port ${PORT} --pidfile ${plexpy_pid} --config ${plexpy_conf}"
|
command_args="--daemon --quiet --nolaunch --port ${PORT} --pidfile ${tautulli_pid} --config ${tautulli_conf}"
|
||||||
|
|
||||||
# Check for wget and refuse to start without it.
|
# Check for wget and refuse to start without it.
|
||||||
if [ ! -x "${WGET}" ]; then
|
if [ ! -x "${WGET}" ]; then
|
||||||
warn "PlexPy not started: You need wget to safely shut down PlexPy."
|
warn "Tautulli not started: You need wget to safely shut down Tautulli."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -58,21 +58,21 @@ if [ `id -u` != "0" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
verify_plexpy_pid() {
|
verify_tautulli_pid() {
|
||||||
# Make sure the pid corresponds to the PlexPy process.
|
# Make sure the pid corresponds to the Tautulli process.
|
||||||
pid=`cat ${plexpy_pid} 2>/dev/null`
|
pid=`cat ${tautulli_pid} 2>/dev/null`
|
||||||
ps -p ${pid} | grep -q "python ${plexpy_dir}/PlexPy.py"
|
ps -p ${pid} | grep -q "python ${tautulli_dir}/Tautulli.py"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to stop PlexPy cleanly by calling shutdown over http.
|
# Try to stop Tautulli cleanly by calling shutdown over http.
|
||||||
plexpy_stop() {
|
tautulli_stop() {
|
||||||
if [ ! -e "${plexpy_conf}" ]; then
|
if [ ! -e "${tautulli_conf}" ]; then
|
||||||
echo "PlexPy' settings file does not exist. Try starting PlexPy, as this should create the file."
|
echo "Tautulli' settings file does not exist. Try starting Tautulli, as this should create the file."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Stopping $name"
|
echo "Stopping $name"
|
||||||
verify_plexpy_pid
|
verify_tautulli_pid
|
||||||
${WGET} -O - -q --user=${SBUSR} --password=${SBPWD} "http://${HOST}:${PORT}/shutdown/" >/dev/null
|
${WGET} -O - -q --user=${SBUSR} --password=${SBPWD} "http://${HOST}:${PORT}/shutdown/" >/dev/null
|
||||||
|
|
||||||
if [ -n "${pid}" ]; then
|
if [ -n "${pid}" ]; then
|
||||||
@@ -81,8 +81,8 @@ plexpy_stop() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
plexpy_status() {
|
tautulli_status() {
|
||||||
verify_plexpy_pid && echo "$name is running as ${pid}" || echo "$name is not running"
|
verify_tautulli_pid && echo "$name is running as ${pid}" || echo "$name is not running"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_rc_command "$1"
|
run_rc_command "$1"
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
### BEGIN INIT INFO
|
### BEGIN INIT INFO
|
||||||
# Provides: PlexPy
|
# Provides: Tautulli
|
||||||
# Required-Start: $all
|
# Required-Start: $all
|
||||||
# Required-Stop: $all
|
# Required-Stop: $all
|
||||||
# Default-Start: 2 3 4 5
|
# Default-Start: 2 3 4 5
|
||||||
# Default-Stop: 0 1 6
|
# Default-Stop: 0 1 6
|
||||||
# Short-Description: starts PlexPy
|
# Short-Description: starts Tautulli
|
||||||
# Description: starts PlexPy
|
# Description: starts Tautulli
|
||||||
### END INIT INFO
|
### END INIT INFO
|
||||||
|
|
||||||
# Source function library.
|
# Source function library.
|
||||||
. /etc/init.d/functions
|
. /etc/init.d/functions
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
prog=plexpy
|
prog=tautulli
|
||||||
lockfile=/var/lock/subsys/$prog
|
lockfile=/var/lock/subsys/$prog
|
||||||
homedir=/opt/plexpy
|
homedir=/opt/Tautulli
|
||||||
datadir=/opt/plexpy
|
datadir=/opt/Tautulli
|
||||||
configfile=/opt/plexpy/config.ini
|
configfile=/opt/Tautulli/config.ini
|
||||||
pidfile=/var/run/plexpy.pid
|
pidfile=/var/run/tautulli.pid
|
||||||
nice=
|
nice=
|
||||||
# The following line must point to your Python 2.7 install
|
# The following line must point to your Python 2.7 install
|
||||||
python27=/usr/src/Python-2.7.11/python
|
python27=/usr/src/Python-2.7.11/python
|
||||||
@@ -30,7 +30,7 @@ options=" --daemon --config $configfile --pidfile $pidfile --datadir $datadir --
|
|||||||
start() {
|
start() {
|
||||||
# Start daemon.
|
# Start daemon.
|
||||||
echo -n $"Starting $prog: "
|
echo -n $"Starting $prog: "
|
||||||
daemon --pidfile=$pidfile $nice $python27 $homedir/PlexPy.py $options
|
daemon --pidfile=$pidfile $nice $python27 $homedir/Tautulli.py $options
|
||||||
RETVAL=$?
|
RETVAL=$?
|
||||||
echo
|
echo
|
||||||
[ $RETVAL -eq 0 ] && touch $lockfile
|
[ $RETVAL -eq 0 ] && touch $lockfile
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
# PROVIDE: plexpy
|
# PROVIDE: tautulli
|
||||||
# REQUIRE: DAEMON plexpy
|
# REQUIRE: DAEMON tautulli
|
||||||
# KEYWORD: shutdown
|
# KEYWORD: shutdown
|
||||||
#
|
#
|
||||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||||
# to enable this service:
|
# to enable this service:
|
||||||
#
|
#
|
||||||
# plexpy_enable (bool): Set to NO by default.
|
# tautulli_enable (bool): Set to NO by default.
|
||||||
# Set it to YES to enable it.
|
# Set it to YES to enable it.
|
||||||
# plexpy_user: The user account PlexPy daemon runs as what
|
# tautulli_user: The user account Tautulli daemon runs as what
|
||||||
# you want it to be. It uses 'plexpy' user by
|
# you want it to be. It uses 'tautulli' user by
|
||||||
# default. Do not sets it as empty or it will run
|
# default. Do not sets it as empty or it will run
|
||||||
# as root.
|
# as root.
|
||||||
# plexpy_dir: Directory where PlexPy lives.
|
# tautulli_dir: Directory where Tautulli lives.
|
||||||
# Default: /usr/local/share/plexpy
|
# Default: /usr/local/share/Tautulli
|
||||||
# plexpy_chdir: Change to this directory before running PlexPy.
|
# tautulli_chdir: Change to this directory before running Tautulli.
|
||||||
# Default is same as plexpy_dir.
|
# Default is same as tautulli_dir.
|
||||||
# plexpy_pid: The name of the pidfile to create.
|
# tautulli_pid: The name of the pidfile to create.
|
||||||
# Default is plexpy.pid in plexpy_dir.
|
# Default is tautulli.pid in tautulli_dir.
|
||||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||||
|
|
||||||
. /etc/rc.subr
|
. /etc/rc.subr
|
||||||
|
|
||||||
name="plexpy"
|
name="tautulli"
|
||||||
rcvar=${name}_enable
|
rcvar=${name}_enable
|
||||||
|
|
||||||
load_rc_config ${name}
|
load_rc_config ${name}
|
||||||
|
|
||||||
: ${plexpy_enable:="NO"}
|
: ${tautulli_enable:="NO"}
|
||||||
: ${plexpy_user:="plexpy"}
|
: ${tautulli_user:="tautulli"}
|
||||||
: ${plexpy_dir:="/usr/local/share/plexpy"}
|
: ${tautulli_dir:="/usr/local/share/Tautulli"}
|
||||||
: ${plexpy_chdir:="${plexpy_dir}"}
|
: ${tautulli_chdir:="${tautulli_dir}"}
|
||||||
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
|
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
|
||||||
: ${plexpy_flags:=""}
|
: ${tautulli_flags:=""}
|
||||||
|
|
||||||
status_cmd="${name}_status"
|
status_cmd="${name}_status"
|
||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
command="${plexpy_dir}/PlexPy.py"
|
command="${tautulli_dir}/Tautulli.py"
|
||||||
command_args="--daemon --pidfile ${plexpy_pid} --quiet --nolaunch ${plexpy_flags}"
|
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
|
||||||
|
|
||||||
# Ensure user is root when running this script.
|
# Ensure user is root when running this script.
|
||||||
if [ `id -u` != "0" ]; then
|
if [ `id -u` != "0" ]; then
|
||||||
@@ -47,21 +47,21 @@ if [ `id -u` != "0" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
verify_plexpy_pid() {
|
verify_tautulli_pid() {
|
||||||
# Make sure the pid corresponds to the PlexPy process.
|
# Make sure the pid corresponds to the Tautulli process.
|
||||||
if [ -f ${plexpy_pid} ]; then
|
if [ -f ${tautulli_pid} ]; then
|
||||||
pid=`cat ${plexpy_pid} 2>/dev/null`
|
pid=`cat ${tautulli_pid} 2>/dev/null`
|
||||||
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
|
ps -p ${pid} | grep -q "python2 ${tautulli_dir}/Tautulli.py"
|
||||||
return $?
|
return $?
|
||||||
else
|
else
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to stop PlexPy cleanly by sending SIGTERM
|
# Try to stop Tautulli cleanly by sending SIGTERM
|
||||||
plexpy_stop() {
|
tautulli_stop() {
|
||||||
echo "Stopping $name"
|
echo "Stopping $name"
|
||||||
verify_plexpy_pid
|
verify_tautulli_pid
|
||||||
if [ -n "${pid}" ]; then
|
if [ -n "${pid}" ]; then
|
||||||
kill ${pid}
|
kill ${pid}
|
||||||
wait_for_pids ${pid}
|
wait_for_pids ${pid}
|
||||||
@@ -69,8 +69,8 @@ plexpy_stop() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
plexpy_status() {
|
tautulli_status() {
|
||||||
verify_plexpy_pid
|
verify_tautulli_pid
|
||||||
if [ -n "${pid}" ]; then
|
if [ -n "${pid}" ]; then
|
||||||
echo "$name is running as ${pid}."
|
echo "$name is running as ${pid}."
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
# PROVIDE: plexpy
|
# PROVIDE: tautulli
|
||||||
# REQUIRE: DAEMON plexpy
|
# REQUIRE: DAEMON tautulli
|
||||||
# KEYWORD: shutdown
|
# KEYWORD: shutdown
|
||||||
#
|
#
|
||||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||||
# to enable this service:
|
# to enable this service:
|
||||||
#
|
#
|
||||||
# plexpy_enable (bool): Set to NO by default.
|
# tautulli_enable (bool): Set to NO by default.
|
||||||
# Set it to YES to enable it.
|
# Set it to YES to enable it.
|
||||||
# plexpy_user: The user account PlexPy daemon runs as what
|
# tautulli_user: The user account Tautulli daemon runs as what
|
||||||
# you want it to be. It uses 'plexpy' user by
|
# you want it to be. It uses 'tautulli' user by
|
||||||
# default. Do not sets it as empty or it will run
|
# default. Do not sets it as empty or it will run
|
||||||
# as root.
|
# as root.
|
||||||
# plexpy_dir: Directory where PlexPy lives.
|
# tautulli_dir: Directory where Tautulli lives.
|
||||||
# Default: /usr/local/share/plexpy
|
# Default: /usr/local/share/Tautulli
|
||||||
# plexpy_chdir: Change to this directory before running PlexPy.
|
# tautulli_chdir: Change to this directory before running Tautulli.
|
||||||
# Default is same as plexpy_dir.
|
# Default is same as tautulli_dir.
|
||||||
# plexpy_pid: The name of the pidfile to create.
|
# tautulli_pid: The name of the pidfile to create.
|
||||||
# Default is plexpy.pid in plexpy_dir.
|
# Default is tautulli.pid in tautulli_dir.
|
||||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||||
|
|
||||||
. /etc/rc.subr
|
. /etc/rc.subr
|
||||||
|
|
||||||
name="plexpy"
|
name="tautulli"
|
||||||
rcvar=${name}_enable
|
rcvar=${name}_enable
|
||||||
|
|
||||||
load_rc_config ${name}
|
load_rc_config ${name}
|
||||||
|
|
||||||
: ${plexpy_enable:="NO"}
|
: ${tautulli_enable:="NO"}
|
||||||
: ${plexpy_user:="plexpy"}
|
: ${tautulli_user:="tautulli"}
|
||||||
: ${plexpy_dir:="/usr/local/share/plexpy"}
|
: ${tautulli_dir:="/usr/local/share/Tautulli"}
|
||||||
: ${plexpy_chdir:="${plexpy_dir}"}
|
: ${tautulli_chdir:="${tautulli_dir}"}
|
||||||
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
|
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
|
||||||
: ${plexpy_flags:=""}
|
: ${tautulli_flags:=""}
|
||||||
|
|
||||||
status_cmd="${name}_status"
|
status_cmd="${name}_status"
|
||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
command="${plexpy_dir}/PlexPy.py"
|
command="${tautulli_dir}/Tautulli.py"
|
||||||
command_args="--daemon --pidfile ${plexpy_pid} --quiet --nolaunch ${plexpy_flags}"
|
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
|
||||||
|
|
||||||
# Ensure user is root when running this script.
|
# Ensure user is root when running this script.
|
||||||
if [ `id -u` != "0" ]; then
|
if [ `id -u` != "0" ]; then
|
||||||
@@ -47,21 +47,21 @@ if [ `id -u` != "0" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
verify_plexpy_pid() {
|
verify_tautulli_pid() {
|
||||||
# Make sure the pid corresponds to the PlexPy process.
|
# Make sure the pid corresponds to the Tautulli process.
|
||||||
if [ -f ${plexpy_pid} ]; then
|
if [ -f ${tautulli_pid} ]; then
|
||||||
pid=`cat ${plexpy_pid} 2>/dev/null`
|
pid=`cat ${tautulli_pid} 2>/dev/null`
|
||||||
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
|
ps -p ${pid} | grep -q "python2 ${tautulli_dir}/Tautulli.py"
|
||||||
return $?
|
return $?
|
||||||
else
|
else
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to stop PlexPy cleanly by sending SIGTERM
|
# Try to stop Tautulli cleanly by sending SIGTERM
|
||||||
plexpy_stop() {
|
tautulli_stop() {
|
||||||
echo "Stopping $name."
|
echo "Stopping $name."
|
||||||
verify_plexpy_pid
|
verify_tautulli_pid
|
||||||
if [ -n "${pid}" ]; then
|
if [ -n "${pid}" ]; then
|
||||||
kill ${pid}
|
kill ${pid}
|
||||||
wait_for_pids ${pid}
|
wait_for_pids ${pid}
|
||||||
@@ -69,8 +69,8 @@ plexpy_stop() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
plexpy_status() {
|
tautulli_status() {
|
||||||
verify_plexpy_pid
|
verify_tautulli_pid
|
||||||
if [ -n "${pid}" ]; then
|
if [ -n "${pid}" ]; then
|
||||||
echo "$name is running as ${pid}."
|
echo "$name is running as ${pid}."
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>plexpy</string>
|
<string>tautulli</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<!-- Modify these two lines if you need to to reflect your python location and PlexPy install location -->
|
<!-- Modify these two lines if you need to to reflect your python location and Tautulli install location -->
|
||||||
<string>/usr/bin/python</string>
|
<string>/usr/bin/python</string>
|
||||||
<string>/Applications/PlexPy/PlexPy.py</string>
|
<string>/Applications/Tautulli/Tautulli.py</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
|
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
|
||||||
<!--
|
<!--
|
||||||
Created by Manifold
|
Created by Manifold
|
||||||
--><service_bundle type="manifest" name="plexpy">
|
--><service_bundle type="manifest" name="tautulli">
|
||||||
|
|
||||||
<service name="application/plexpy" type="service" version="1">
|
<service name="application/tautulli" type="service" version="1">
|
||||||
|
|
||||||
<create_default_instance enabled="true"/>
|
<create_default_instance enabled="true"/>
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<method_context>
|
<method_context>
|
||||||
<method_credential user="plexpy" group="nogroup"/>
|
<method_credential user="tautulli" group="nogroup"/>
|
||||||
</method_context>
|
</method_context>
|
||||||
|
|
||||||
<exec_method type="method" name="start" exec="python /opt/plexpy/PlexPy.py --daemon --quiet --nolaunch" timeout_seconds="60"/>
|
<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"/>
|
<exec_method type="method" name="stop" exec=":kill" timeout_seconds="60"/>
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<common_name>
|
<common_name>
|
||||||
<loctext xml:lang="C">
|
<loctext xml:lang="C">
|
||||||
PlexPy
|
Tautulli
|
||||||
</loctext>
|
</loctext>
|
||||||
</common_name>
|
</common_name>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# PlexPy - Stats for Plex Media Server usage
|
# Tautulli - Stats for Plex Media Server usage
|
||||||
#
|
#
|
||||||
# Service Unit file for systemd system manager
|
# Service Unit file for systemd system manager
|
||||||
#
|
#
|
||||||
# INSTALLATION NOTES
|
# INSTALLATION NOTES
|
||||||
#
|
#
|
||||||
# 1. Rename this file as you want, ensuring that it ends in .service
|
# 1. Rename this file as you want, ensuring that it ends in .service
|
||||||
# e.g. 'plexpy.service'
|
# e.g. 'tautulli.service'
|
||||||
#
|
#
|
||||||
# 2. Adjust configuration settings as required. More details in the
|
# 2. Adjust configuration settings as required. More details in the
|
||||||
# "CONFIGURATION NOTES" section shown below.
|
# "CONFIGURATION NOTES" section shown below.
|
||||||
@@ -15,39 +15,39 @@
|
|||||||
#
|
#
|
||||||
# 4. Enable boot-time autostart with the following commands:
|
# 4. Enable boot-time autostart with the following commands:
|
||||||
# systemctl daemon-reload
|
# systemctl daemon-reload
|
||||||
# systemctl enable plexpy.service
|
# systemctl enable tautulli.service
|
||||||
#
|
#
|
||||||
# 5. Start now with the following command:
|
# 5. Start now with the following command:
|
||||||
# systemctl start plexpy.service
|
# systemctl start tautulli.service
|
||||||
#
|
#
|
||||||
# CONFIGURATION NOTES
|
# CONFIGURATION NOTES
|
||||||
#
|
#
|
||||||
# - The example settings in this file assume that you will run PlexPy as user: plexpy
|
# - The example settings in this file assume that you will run Tautulli as user: tautulli
|
||||||
# - To create this user and give it ownership of the plexpy directory:
|
# - To create this user and give it ownership of the tautulli directory:
|
||||||
# sudo adduser --system --no-create-home plexpy
|
# sudo adduser --system --no-create-home tautulli
|
||||||
# sudo chown plexpy:nogroup -R /opt/plexpy
|
# sudo chown tautulli:nogroup -R /opt/Tautulli
|
||||||
#
|
#
|
||||||
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
|
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
|
||||||
#
|
#
|
||||||
# - Adjust ExecStart= to point to:
|
# - Adjust ExecStart= to point to:
|
||||||
# 1. Your PlexPy executable,
|
# 1. Your Tautulli executable,
|
||||||
# 2. Your config file (recommended is to put it somewhere in /etc)
|
# 2. Your config file (recommended is to put it somewhere in /etc)
|
||||||
# 3. Your datadir (recommended is to NOT put it in your PlexPy exec dir)
|
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
|
||||||
#
|
#
|
||||||
# - Adjust User= and Group= to the user/group you want PlexPy to run as.
|
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
|
||||||
#
|
#
|
||||||
# - WantedBy= specifies which target (i.e. runlevel) to start PlexPy for.
|
# - WantedBy= specifies which target (i.e. runlevel) to start Tautulli for.
|
||||||
# multi-user.target equates to runlevel 3 (multi-user text mode)
|
# multi-user.target equates to runlevel 3 (multi-user text mode)
|
||||||
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=PlexPy - Stats for Plex Media Server usage
|
Description=Tautulli - Stats for Plex Media Server usage
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/opt/plexpy/PlexPy.py --quiet --daemon --nolaunch --config /opt/plexpy/config.ini --datadir /opt/plexpy
|
ExecStart=/opt/Tautulli/Tautulli.py --quiet --daemon --nolaunch --config /opt/Tautulli/config.ini --datadir /opt/Tautulli
|
||||||
GuessMainPID=no
|
GuessMainPID=no
|
||||||
Type=forking
|
Type=forking
|
||||||
User=plexpy
|
User=tautulli
|
||||||
Group=nogroup
|
Group=nogroup
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -1,71 +1,71 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
## Don't edit this file
|
## Don't edit this file
|
||||||
## Edit user configuation in /etc/default/plexpy to change
|
## Edit user configuation in /etc/default/tautulli to change
|
||||||
##
|
##
|
||||||
## Make sure init script is executable
|
## Make sure init script is executable
|
||||||
## sudo chmod +x /path/to/init.ubuntu
|
## sudo chmod +x /path/to/init.ubuntu
|
||||||
##
|
##
|
||||||
## Install the init script
|
## Install the init script
|
||||||
## sudo ln -s /path/to/init.ubuntu /etc/init.d/plexpy
|
## sudo ln -s /path/to/init.ubuntu /etc/init.d/tautulli
|
||||||
##
|
##
|
||||||
## Create the plexpy daemon user:
|
## Create the tautulli daemon user:
|
||||||
## sudo adduser --system --no-create-home plexpy
|
## sudo adduser --system --no-create-home tautulli
|
||||||
##
|
##
|
||||||
## Make sure /opt/plexpy is owned by the plexpy user
|
## Make sure /opt/Tautulli is owned by the tautulli user
|
||||||
## sudo chown plexpy:nogroup -R /opt/plexpy
|
## sudo chown tautulli:nogroup -R /opt/Tautulli
|
||||||
##
|
##
|
||||||
## Touch the default file to stop the warning message when starting
|
## Touch the default file to stop the warning message when starting
|
||||||
## sudo touch /etc/default/plexpy
|
## sudo touch /etc/default/tautulli
|
||||||
##
|
##
|
||||||
## To start PlexPy automatically
|
## To start Tautulli automatically
|
||||||
## sudo update-rc.d plexpy defaults
|
## sudo update-rc.d tautulli defaults
|
||||||
##
|
##
|
||||||
## To start/stop/restart PlexPy
|
## To start/stop/restart Tautulli
|
||||||
## sudo service plexpy start
|
## sudo service tautulli start
|
||||||
## sudo service plexpy stop
|
## sudo service tautulli stop
|
||||||
## sudo service plexpy restart
|
## sudo service tautulli restart
|
||||||
##
|
##
|
||||||
## HP_USER= #$RUN_AS, username to run plexpy under, the default is plexpy
|
## TAUTULLI_USER= #$RUN_AS, username to run Tautulli under, the default is tautulli
|
||||||
## HP_HOME= #$APP_PATH, the location of PlexPy.py, the default is /opt/plexpy
|
## TAUTULLI_HOME= #$APP_PATH, the location of Tautulli.py, the default is /opt/Tautulli
|
||||||
## HP_DATA= #$DATA_DIR, the location of plexpy.db, cache, logs, the default is /opt/plexpy
|
## TAUTULLI_DATA= #$DATA_DIR, the location of plexpy.db, cache, logs, the default is /opt/Tautulli
|
||||||
## HP_PIDFILE= #$PID_FILE, the location of plexpy.pid, the default is /var/run/plexpy/plexpy.pid
|
## 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
|
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
|
||||||
## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for plexpy, i.e. " --config=/home/plexpy/config.ini"
|
## 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"
|
## SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
|
||||||
## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
|
## TAUTULLI_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
|
||||||
##
|
##
|
||||||
## EXAMPLE if want to run as different user
|
## EXAMPLE if want to run as different user
|
||||||
## add HP_USER=username to /etc/default/plexpy
|
## add TAUTULLI_USER=username to /etc/default/tautulli
|
||||||
## otherwise default plexpy is used
|
## otherwise default tautulli is used
|
||||||
#
|
#
|
||||||
### BEGIN INIT INFO
|
### BEGIN INIT INFO
|
||||||
# Provides: plexpy
|
# Provides: tautulli
|
||||||
# Required-Start: $local_fs $network $remote_fs
|
# Required-Start: $local_fs $network $remote_fs
|
||||||
# Required-Stop: $local_fs $network $remote_fs
|
# Required-Stop: $local_fs $network $remote_fs
|
||||||
# Should-Start: $NetworkManager
|
# Should-Start: $NetworkManager
|
||||||
# Should-Stop: $NetworkManager
|
# Should-Stop: $NetworkManager
|
||||||
# Default-Start: 2 3 4 5
|
# Default-Start: 2 3 4 5
|
||||||
# Default-Stop: 0 1 6
|
# Default-Stop: 0 1 6
|
||||||
# Short-Description: starts instance of PlexPy
|
# Short-Description: starts instance of Tautulli
|
||||||
# Description: starts instance of PlexPy using start-stop-daemon
|
# Description: starts instance of Tautulli using start-stop-daemon
|
||||||
### END INIT INFO
|
### END INIT INFO
|
||||||
|
|
||||||
# Script name
|
# Script name
|
||||||
NAME=plexpy
|
NAME=tautulli
|
||||||
|
|
||||||
# App name
|
# App name
|
||||||
DESC=PlexPy
|
DESC=Tautulli
|
||||||
|
|
||||||
SETTINGS_LOADED=FALSE
|
SETTINGS_LOADED=FALSE
|
||||||
|
|
||||||
. /lib/lsb/init-functions
|
. /lib/lsb/init-functions
|
||||||
|
|
||||||
# Source PlexPy configuration
|
# Source Tautulli configuration
|
||||||
if [ -f /etc/default/plexpy ]; then
|
if [ -f /etc/default/tautulli ]; then
|
||||||
SETTINGS=/etc/default/plexpy
|
SETTINGS=/etc/default/tautulli
|
||||||
else
|
else
|
||||||
log_warning_msg "/etc/default/plexpy not found using default settings.";
|
log_warning_msg "/etc/default/tautulli not found using default settings.";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
check_retval() {
|
check_retval() {
|
||||||
@@ -84,32 +84,32 @@ load_settings() {
|
|||||||
|
|
||||||
## The defaults
|
## The defaults
|
||||||
# Run as username
|
# Run as username
|
||||||
RUN_AS=${HP_USER-plexpy}
|
RUN_AS=${TAUTULLI_USER-tautulli}
|
||||||
|
|
||||||
# Path to app HP_HOME=path_to_app_PlexPy.py
|
# Path to app TAUTULLI_HOME=path_to_app_Tautulli.py
|
||||||
APP_PATH=${HP_HOME-/opt/plexpy}
|
APP_PATH=${TAUTULLI_HOME-/opt/Tautulli}
|
||||||
|
|
||||||
# Data directory where plexpy.db, cache and logs are stored
|
# Data directory where plexpy.db, cache and logs are stored
|
||||||
DATA_DIR=${HP_DATA-/opt/plexpy}
|
DATA_DIR=${TAUTULLI_DATA-/opt/Tautulli}
|
||||||
|
|
||||||
# Path to store PID file
|
# Path to store PID file
|
||||||
PID_FILE=${HP_PIDFILE-/var/run/plexpy/plexpy.pid}
|
PID_FILE=${TAUTULLI_PIDFILE-/var/run/tautulli/tautulli.pid}
|
||||||
|
|
||||||
# Path to python bin
|
# Path to python bin
|
||||||
DAEMON=${PYTHON_BIN-/usr/bin/python}
|
DAEMON=${PYTHON_BIN-/usr/bin/python}
|
||||||
|
|
||||||
# Extra daemon option like: HP_OPTS=" --config=/home/plexpy/config.ini"
|
# Extra daemon option like: TAUTULLI_OPTS=" --config=/home/Tautulli/config.ini"
|
||||||
EXTRA_DAEMON_OPTS=${HP_OPTS-}
|
EXTRA_DAEMON_OPTS=${TAUTULLI_OPTS-}
|
||||||
|
|
||||||
# Extra start-stop-daemon option like START_OPTS=" --group=users"
|
# Extra start-stop-daemon option like START_OPTS=" --group=users"
|
||||||
EXTRA_SSD_OPTS=${SSD_OPTS-}
|
EXTRA_SSD_OPTS=${SSD_OPTS-}
|
||||||
|
|
||||||
# Hardcoded port to run on, overrides config.ini settings
|
# Hardcoded port to run on, overrides config.ini settings
|
||||||
[ -n "$HP_PORT" ] && {
|
[ -n "$TAUTULLI_PORT" ] && {
|
||||||
PORT_OPTS=" --port=${HP_PORT} "
|
PORT_OPTS=" --port=${TAUTULLI_PORT} "
|
||||||
}
|
}
|
||||||
|
|
||||||
DAEMON_OPTS=" PlexPy.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
|
DAEMON_OPTS=" Tautulli.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
|
||||||
|
|
||||||
SETTINGS_LOADED=TRUE
|
SETTINGS_LOADED=TRUE
|
||||||
fi
|
fi
|
||||||
@@ -162,7 +162,7 @@ handle_updates () {
|
|||||||
return 0; }
|
return 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
start_plexpy () {
|
start_tautulli () {
|
||||||
handle_pid
|
handle_pid
|
||||||
handle_datadir
|
handle_datadir
|
||||||
handle_updates
|
handle_updates
|
||||||
@@ -175,7 +175,7 @@ start_plexpy () {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_plexpy () {
|
stop_tautulli () {
|
||||||
if is_running; then
|
if is_running; then
|
||||||
log_daemon_msg "Stopping $DESC"
|
log_daemon_msg "Stopping $DESC"
|
||||||
start-stop-daemon -o --stop --pidfile $PID_FILE --retry 15
|
start-stop-daemon -o --stop --pidfile $PID_FILE --retry 15
|
||||||
@@ -187,14 +187,14 @@ stop_plexpy () {
|
|||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
start_plexpy
|
start_tautulli
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
stop_plexpy
|
stop_tautulli
|
||||||
;;
|
;;
|
||||||
restart|force-reload)
|
restart|force-reload)
|
||||||
stop_plexpy
|
stop_tautulli
|
||||||
start_plexpy
|
start_tautulli
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
status_of_proc -p "$PID_FILE" "$DAEMON" "$DESC"
|
status_of_proc -p "$PID_FILE" "$DAEMON" "$DESC"
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
# plexpy
|
# tautulli
|
||||||
#
|
#
|
||||||
# This is a session/user job. Install this file into /usr/share/upstart/sessions
|
# This is a session/user job. Install this file into /usr/share/upstart/sessions
|
||||||
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if
|
# if Tautulli is installed system wide, and into $XDG_CONFIG_HOME/upstart if
|
||||||
# plexpy is installed per user. Change the executable path appropiately.
|
# Tautulli is installed per user. Change the executable path appropiately.
|
||||||
|
|
||||||
start on desktop-start
|
start on desktop-start
|
||||||
stop on desktop-end
|
stop on desktop-end
|
||||||
|
|
||||||
env CONFIG=""$XDG_CONFIG_HOME"/plexpy"
|
env CONFIG=""$XDG_CONFIG_HOME"/Tautulli"
|
||||||
env DATA=""$XDG_DATA_HOME"/plexpy"
|
env DATA=""$XDG_DATA_HOME"/Tautulli"
|
||||||
|
|
||||||
pre-start script
|
pre-start script
|
||||||
[ -d "$CONFIG" ] || mkdir -p "$CONFIG"
|
[ -d "$CONFIG" ] || mkdir -p "$CONFIG"
|
||||||
[ -d "$DATA" ] || mkdir -p "$DATA"
|
[ -d "$DATA" ] || mkdir -p "$DATA"
|
||||||
end script
|
end script
|
||||||
|
|
||||||
exec PlexPy.py --nolaunch --config "$CONFIG"/config.ini --datadir "$DATA"
|
exec Tautulli.py --nolaunch --config "$CONFIG"/config.ini --datadir "$DATA"
|
||||||
|
|||||||
121
lib/UniversalAnalytics/HTTPLog.py
Normal file
121
lib/UniversalAnalytics/HTTPLog.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
###############################################################################
|
||||||
|
# Formatting filter for urllib2's HTTPHandler(debuglevel=1) output
|
||||||
|
# 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.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
import sys, re, os
|
||||||
|
from cStringIO import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BufferTranslator(object):
|
||||||
|
""" Provides a buffer-compatible interface for filtering buffer content.
|
||||||
|
"""
|
||||||
|
parsers = []
|
||||||
|
|
||||||
|
def __init__(self, output):
|
||||||
|
self.output = output
|
||||||
|
self.encoding = getattr(output, 'encoding', None)
|
||||||
|
|
||||||
|
def write(self, content):
|
||||||
|
content = self.translate(content)
|
||||||
|
self.output.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stripslashes(content):
|
||||||
|
return content.decode('string_escape')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def addslashes(content):
|
||||||
|
return content.encode('string_escape')
|
||||||
|
|
||||||
|
def translate(self, line):
|
||||||
|
for pattern, method in self.parsers:
|
||||||
|
match = pattern.match(line)
|
||||||
|
if match:
|
||||||
|
return method(match)
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LineBufferTranslator(BufferTranslator):
|
||||||
|
""" Line buffer implementation supports translation of line-format input
|
||||||
|
even when input is not already line-buffered. Caches input until newlines
|
||||||
|
occur, and then dispatches translated input to output buffer.
|
||||||
|
"""
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
self._linepending = []
|
||||||
|
super(LineBufferTranslator, self).__init__(*a, **kw)
|
||||||
|
|
||||||
|
def write(self, _input):
|
||||||
|
lines = _input.splitlines(True)
|
||||||
|
for i in range(0, len(lines)):
|
||||||
|
last = i
|
||||||
|
if lines[i].endswith('\n'):
|
||||||
|
prefix = len(self._linepending) and ''.join(self._linepending) or ''
|
||||||
|
self.output.write(self.translate(prefix + lines[i]))
|
||||||
|
del self._linepending[0:]
|
||||||
|
last = -1
|
||||||
|
|
||||||
|
if last >= 0:
|
||||||
|
self._linepending.append(lines[ last ])
|
||||||
|
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if len(self._linepending):
|
||||||
|
self.output.write(self.translate(''.join(self._linepending)))
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPTranslator(LineBufferTranslator):
|
||||||
|
""" Translates output from |urllib2| HTTPHandler(debuglevel = 1) into
|
||||||
|
HTTP-compatible, readible text structures for human analysis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RE_LINE_PARSER = re.compile(r'^(?:([a-z]+):)\s*(\'?)([^\r\n]*)\2(?:[\r\n]*)$')
|
||||||
|
RE_LINE_BREAK = re.compile(r'(\r?\n|(?:\\r)?\\n)')
|
||||||
|
RE_HTTP_METHOD = re.compile(r'^(POST|GET|HEAD|DELETE|PUT|TRACE|OPTIONS)')
|
||||||
|
RE_PARAMETER_SPACER = re.compile(r'&([a-z0-9]+)=')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def spacer(cls, line):
|
||||||
|
return cls.RE_PARAMETER_SPACER.sub(r' &\1= ', line)
|
||||||
|
|
||||||
|
def translate(self, line):
|
||||||
|
|
||||||
|
parsed = self.RE_LINE_PARSER.match(line)
|
||||||
|
|
||||||
|
if parsed:
|
||||||
|
value = parsed.group(3)
|
||||||
|
stage = parsed.group(1)
|
||||||
|
|
||||||
|
if stage == 'send': # query string is rendered here
|
||||||
|
return '\n# HTTP Request:\n' + self.stripslashes(value)
|
||||||
|
elif stage == 'reply':
|
||||||
|
return '\n\n# HTTP Response:\n' + self.stripslashes(value)
|
||||||
|
elif stage == 'header':
|
||||||
|
return value + '\n'
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
def consume(outbuffer = None): # Capture standard output
|
||||||
|
sys.stdout = HTTPTranslator(outbuffer or sys.stdout)
|
||||||
|
return sys.stdout
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
consume(sys.stdout).write(sys.stdin.read())
|
||||||
|
print '\n'
|
||||||
|
|
||||||
|
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4
|
||||||
433
lib/UniversalAnalytics/Tracker.py
Normal file
433
lib/UniversalAnalytics/Tracker.py
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
###############################################################################
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import random
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
return '%8s-%4s-%4s-%4s-%12s' % (
|
||||||
|
checksum[0:8], checksum[8:12], checksum[12:16], checksum[16:20], checksum[20:32])
|
||||||
|
|
||||||
|
|
||||||
|
class Time(datetime.datetime):
|
||||||
|
""" Wrappers and convenience methods for processing various time representations """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_unix(cls, seconds, milliseconds=0):
|
||||||
|
""" Produce a full |datetime.datetime| object from a Unix timestamp """
|
||||||
|
base = list(time.gmtime(seconds))[0:6]
|
||||||
|
base.append(milliseconds * 1000) # microseconds
|
||||||
|
return cls(*base)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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'
|
||||||
|
base = time.mktime(timestamp.timetuple())
|
||||||
|
return base
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def milliseconds_offset(cls, timestamp, now=None):
|
||||||
|
""" Offset time (in milliseconds) from a |datetime.datetime| object to now """
|
||||||
|
if isinstance(timestamp, (int, float)):
|
||||||
|
base = timestamp
|
||||||
|
else:
|
||||||
|
base = cls.to_unix(timestamp)
|
||||||
|
base = base + (timestamp.microsecond / 1000000)
|
||||||
|
if now is None:
|
||||||
|
now = time.time()
|
||||||
|
return (now - base) * 1000
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPRequest(object):
|
||||||
|
""" URL Construction and request handling abstraction.
|
||||||
|
This is not intended to be used outside this module.
|
||||||
|
|
||||||
|
Automates mapping of persistent state (i.e. query parameters)
|
||||||
|
onto transcient datasets for each query.
|
||||||
|
"""
|
||||||
|
|
||||||
|
endpoint = 'https://www.google-analytics.com/collect'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def debug():
|
||||||
|
""" Activate debugging on urllib2 """
|
||||||
|
handler = HTTPSHandler(debuglevel=1)
|
||||||
|
opener = build_opener(handler)
|
||||||
|
install_opener(opener)
|
||||||
|
|
||||||
|
# Store properties for all requests
|
||||||
|
def __init__(self, user_agent=None, *args, **opts):
|
||||||
|
self.user_agent = user_agent or 'Analytics Pros - Universal Analytics (Python)'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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):
|
||||||
|
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)),
|
||||||
|
headers={
|
||||||
|
'User-Agent': self.user_agent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.open(request)
|
||||||
|
|
||||||
|
def open(self, request):
|
||||||
|
try:
|
||||||
|
return urlopen(request)
|
||||||
|
except HTTPError as e:
|
||||||
|
return False
|
||||||
|
except URLError as e:
|
||||||
|
self.cache_request(request)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def cache_request(self, request):
|
||||||
|
# TODO: implement a proper caching mechanism here for re-transmitting hits
|
||||||
|
# record = (Time.now(), request.get_full_url(), request.get_data(), request.headers)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPPost(HTTPRequest):
|
||||||
|
|
||||||
|
# Apply stored properties to the given dataset & POST to the configured endpoint
|
||||||
|
def send(self, data):
|
||||||
|
request = Request(
|
||||||
|
self.endpoint,
|
||||||
|
data=urlencode(self.fixUTF8(data)),
|
||||||
|
headers={
|
||||||
|
'User-Agent': self.user_agent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.open(request)
|
||||||
|
|
||||||
|
|
||||||
|
class Tracker(object):
|
||||||
|
""" Primary tracking interface for Universal Analytics """
|
||||||
|
params = None
|
||||||
|
parameter_alias = {}
|
||||||
|
valid_hittypes = ('pageview', 'event', 'social', 'screenview', 'transaction', 'item', 'exception', 'timing')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def alias(cls, typemap, base, *names):
|
||||||
|
""" Declare an alternate (humane) name for a measurement protocol parameter """
|
||||||
|
cls.parameter_alias[base] = (typemap, base)
|
||||||
|
for i in names:
|
||||||
|
cls.parameter_alias[i] = (typemap, base)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def coerceParameter(cls, name, value=None):
|
||||||
|
if isinstance(name, basestring) 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)
|
||||||
|
|
||||||
|
def payload(self, data):
|
||||||
|
for key, value in data.iteritems():
|
||||||
|
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')]
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def consume_options(cls, data, hittype, args):
|
||||||
|
""" Interpret sequential arguments related to known hittypes based on declared structures """
|
||||||
|
opt_position = 0
|
||||||
|
data['t'] = hittype # integrate hit type parameter
|
||||||
|
if hittype in cls.option_sequence:
|
||||||
|
for expected_type, optname in cls.option_sequence[hittype]:
|
||||||
|
if opt_position < len(args) and isinstance(args[opt_position], expected_type):
|
||||||
|
data[optname] = args[opt_position]
|
||||||
|
opt_position += 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def hittime(cls, timestamp=None, age=None, milliseconds=None):
|
||||||
|
""" Returns an integer represeting the milliseconds offset for a given hit (relative to now) """
|
||||||
|
if isinstance(timestamp, (int, float)):
|
||||||
|
return int(Time.milliseconds_offset(Time.from_unix(timestamp, milliseconds=milliseconds)))
|
||||||
|
if isinstance(timestamp, datetime.datetime):
|
||||||
|
return int(Time.milliseconds_offset(timestamp))
|
||||||
|
if isinstance(age, (int, float)):
|
||||||
|
return int(age * 1000) + (milliseconds or 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account(self):
|
||||||
|
return self.params.get('tid', None)
|
||||||
|
|
||||||
|
def __init__(self, account, name=None, client_id=None, hash_client_id=False, user_id=None, user_agent=None,
|
||||||
|
use_post=True):
|
||||||
|
|
||||||
|
if use_post is False:
|
||||||
|
self.http = HTTPRequest(user_agent=user_agent)
|
||||||
|
else:
|
||||||
|
self.http = HTTPPost(user_agent=user_agent)
|
||||||
|
|
||||||
|
self.params = {'v': 1, 'tid': account}
|
||||||
|
|
||||||
|
if client_id is None:
|
||||||
|
client_id = generate_uuid()
|
||||||
|
|
||||||
|
self.params['cid'] = client_id
|
||||||
|
|
||||||
|
self.hash_client_id = hash_client_id
|
||||||
|
|
||||||
|
if user_id is not None:
|
||||||
|
self.params['uid'] = user_id
|
||||||
|
|
||||||
|
def set_timestamp(self, data):
|
||||||
|
""" Interpret time-related options, apply queue-time parameter as needed """
|
||||||
|
if 'hittime' in data: # an absolute timestamp
|
||||||
|
data['qt'] = self.hittime(timestamp=data.pop('hittime', None))
|
||||||
|
if 'hitage' in data: # a relative age (in seconds)
|
||||||
|
data['qt'] = self.hittime(age=data.pop('hitage', None))
|
||||||
|
|
||||||
|
def send(self, hittype, *args, **data):
|
||||||
|
""" Transmit HTTP requests to Google Analytics using the measurement protocol """
|
||||||
|
|
||||||
|
if hittype not in self.valid_hittypes:
|
||||||
|
raise KeyError('Unsupported Universal Analytics Hit Type: {0}'.format(repr(hittype)))
|
||||||
|
|
||||||
|
self.set_timestamp(data)
|
||||||
|
self.consume_options(data, hittype, args)
|
||||||
|
|
||||||
|
for item in args: # process dictionary-object arguments of transcient data
|
||||||
|
if isinstance(item, dict):
|
||||||
|
for key, val in self.payload(item):
|
||||||
|
data[key] = val
|
||||||
|
|
||||||
|
for k, v in self.params.iteritems(): # update only absent parameters
|
||||||
|
if k not in data:
|
||||||
|
data[k] = v
|
||||||
|
|
||||||
|
data = dict(self.payload(data))
|
||||||
|
|
||||||
|
if self.hash_client_id:
|
||||||
|
data['cid'] = generate_uuid(data['cid'])
|
||||||
|
|
||||||
|
# Transmit the hit to Google...
|
||||||
|
self.http.send(data)
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
try:
|
||||||
|
param, value = self.coerceParameter(key, value)
|
||||||
|
self.params[param] = value
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
elif isinstance(name, basestring):
|
||||||
|
try:
|
||||||
|
param, value = self.coerceParameter(name, value)
|
||||||
|
self.params[param] = value
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
param, value = self.coerceParameter(name, None)
|
||||||
|
return self.params.get(param, None)
|
||||||
|
|
||||||
|
def __setitem__(self, name, value):
|
||||||
|
param, value = self.coerceParameter(name, value)
|
||||||
|
self.params[param] = value
|
||||||
|
|
||||||
|
def __delitem__(self, name):
|
||||||
|
param, value = self.coerceParameter(name, None)
|
||||||
|
if param in self.params:
|
||||||
|
del self.params[param]
|
||||||
|
|
||||||
|
|
||||||
|
def safe_unicode(obj):
|
||||||
|
""" Safe convertion to the Unicode string version of the object """
|
||||||
|
try:
|
||||||
|
return unicode(obj)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return obj.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
# Declaring name mappings for Measurement Protocol parameters
|
||||||
|
MAX_CUSTOM_DEFINITIONS = 200
|
||||||
|
MAX_EC_LISTS = 11 # 1-based index
|
||||||
|
MAX_EC_PRODUCTS = 11 # 1-based index
|
||||||
|
MAX_EC_PROMOTIONS = 11 # 1-based index
|
||||||
|
|
||||||
|
Tracker.alias(int, 'v', 'protocol-version')
|
||||||
|
Tracker.alias(safe_unicode, 'cid', 'client-id', 'clientId', 'clientid')
|
||||||
|
Tracker.alias(safe_unicode, 'tid', 'trackingId', 'account')
|
||||||
|
Tracker.alias(safe_unicode, 'uid', 'user-id', 'userId', 'userid')
|
||||||
|
Tracker.alias(safe_unicode, 'uip', 'user-ip', 'userIp', 'ipaddr')
|
||||||
|
Tracker.alias(safe_unicode, 'ua', 'userAgent', 'userAgentOverride', 'user-agent')
|
||||||
|
Tracker.alias(safe_unicode, 'dp', 'page', 'path')
|
||||||
|
Tracker.alias(safe_unicode, 'dt', 'title', 'pagetitle', 'pageTitle' 'page-title')
|
||||||
|
Tracker.alias(safe_unicode, 'dl', 'location')
|
||||||
|
Tracker.alias(safe_unicode, 'dh', 'hostname')
|
||||||
|
Tracker.alias(safe_unicode, 'sc', 'sessioncontrol', 'session-control', 'sessionControl')
|
||||||
|
Tracker.alias(safe_unicode, 'dr', 'referrer', 'referer')
|
||||||
|
Tracker.alias(int, 'qt', 'queueTime', 'queue-time')
|
||||||
|
Tracker.alias(safe_unicode, 't', 'hitType', 'hittype')
|
||||||
|
Tracker.alias(int, 'aip', 'anonymizeIp', 'anonIp', 'anonymize-ip')
|
||||||
|
Tracker.alias(safe_unicode, 'ds', 'dataSource', 'data-source')
|
||||||
|
|
||||||
|
# Campaign attribution
|
||||||
|
Tracker.alias(safe_unicode, 'cn', 'campaign', 'campaignName', 'campaign-name')
|
||||||
|
Tracker.alias(safe_unicode, 'cs', 'source', 'campaignSource', 'campaign-source')
|
||||||
|
Tracker.alias(safe_unicode, 'cm', 'medium', 'campaignMedium', 'campaign-medium')
|
||||||
|
Tracker.alias(safe_unicode, 'ck', 'keyword', 'campaignKeyword', 'campaign-keyword')
|
||||||
|
Tracker.alias(safe_unicode, 'cc', 'content', 'campaignContent', 'campaign-content')
|
||||||
|
Tracker.alias(safe_unicode, 'ci', 'campaignId', 'campaignID', 'campaign-id')
|
||||||
|
|
||||||
|
# Technical specs
|
||||||
|
Tracker.alias(safe_unicode, 'sr', 'screenResolution', 'screen-resolution', 'resolution')
|
||||||
|
Tracker.alias(safe_unicode, 'vp', 'viewport', 'viewportSize', 'viewport-size')
|
||||||
|
Tracker.alias(safe_unicode, 'de', 'encoding', 'documentEncoding', 'document-encoding')
|
||||||
|
Tracker.alias(int, 'sd', 'colors', 'screenColors', 'screen-colors')
|
||||||
|
Tracker.alias(safe_unicode, 'ul', 'language', 'user-language', 'userLanguage')
|
||||||
|
|
||||||
|
# Mobile app
|
||||||
|
Tracker.alias(safe_unicode, 'an', 'appName', 'app-name', 'app')
|
||||||
|
Tracker.alias(safe_unicode, 'cd', 'contentDescription', 'screenName', 'screen-name', 'content-description')
|
||||||
|
Tracker.alias(safe_unicode, 'av', 'appVersion', 'app-version', 'version')
|
||||||
|
Tracker.alias(safe_unicode, 'aid', 'appID', 'appId', 'application-id', 'app-id', 'applicationId')
|
||||||
|
Tracker.alias(safe_unicode, 'aiid', 'appInstallerId', 'app-installer-id')
|
||||||
|
|
||||||
|
# Ecommerce
|
||||||
|
Tracker.alias(safe_unicode, 'ta', 'affiliation', 'transactionAffiliation', 'transaction-affiliation')
|
||||||
|
Tracker.alias(safe_unicode, 'ti', 'transaction', 'transactionId', 'transaction-id')
|
||||||
|
Tracker.alias(float, 'tr', 'revenue', 'transactionRevenue', 'transaction-revenue')
|
||||||
|
Tracker.alias(float, 'ts', 'shipping', 'transactionShipping', 'transaction-shipping')
|
||||||
|
Tracker.alias(float, 'tt', 'tax', 'transactionTax', 'transaction-tax')
|
||||||
|
Tracker.alias(safe_unicode, 'cu', 'currency', 'transactionCurrency',
|
||||||
|
'transaction-currency') # Currency code, e.g. USD, EUR
|
||||||
|
Tracker.alias(safe_unicode, 'in', 'item-name', 'itemName')
|
||||||
|
Tracker.alias(float, 'ip', 'item-price', 'itemPrice')
|
||||||
|
Tracker.alias(float, 'iq', 'item-quantity', 'itemQuantity')
|
||||||
|
Tracker.alias(safe_unicode, 'ic', 'item-code', 'sku', 'itemCode')
|
||||||
|
Tracker.alias(safe_unicode, 'iv', 'item-variation', 'item-category', 'itemCategory', 'itemVariation')
|
||||||
|
|
||||||
|
# Events
|
||||||
|
Tracker.alias(safe_unicode, 'ec', 'event-category', 'eventCategory', 'category')
|
||||||
|
Tracker.alias(safe_unicode, 'ea', 'event-action', 'eventAction', 'action')
|
||||||
|
Tracker.alias(safe_unicode, 'el', 'event-label', 'eventLabel', 'label')
|
||||||
|
Tracker.alias(int, 'ev', 'event-value', 'eventValue', 'value')
|
||||||
|
Tracker.alias(int, 'ni', 'noninteractive', 'nonInteractive', 'noninteraction', 'nonInteraction')
|
||||||
|
|
||||||
|
# Social
|
||||||
|
Tracker.alias(safe_unicode, 'sa', 'social-action', 'socialAction')
|
||||||
|
Tracker.alias(safe_unicode, 'sn', 'social-network', 'socialNetwork')
|
||||||
|
Tracker.alias(safe_unicode, 'st', 'social-target', 'socialTarget')
|
||||||
|
|
||||||
|
# Exceptions
|
||||||
|
Tracker.alias(safe_unicode, 'exd', 'exception-description', 'exceptionDescription', 'exDescription')
|
||||||
|
Tracker.alias(int, 'exf', 'exception-fatal', 'exceptionFatal', 'exFatal')
|
||||||
|
|
||||||
|
# User Timing
|
||||||
|
Tracker.alias(safe_unicode, 'utc', 'timingCategory', 'timing-category')
|
||||||
|
Tracker.alias(safe_unicode, 'utv', 'timingVariable', 'timing-variable')
|
||||||
|
Tracker.alias(float, 'utt', 'time', 'timingTime', 'timing-time')
|
||||||
|
Tracker.alias(safe_unicode, 'utl', 'timingLabel', 'timing-label')
|
||||||
|
Tracker.alias(float, 'dns', 'timingDNS', 'timing-dns')
|
||||||
|
Tracker.alias(float, 'pdt', 'timingPageLoad', 'timing-page-load')
|
||||||
|
Tracker.alias(float, 'rrt', 'timingRedirect', 'timing-redirect')
|
||||||
|
Tracker.alias(safe_unicode, 'tcp', 'timingTCPConnect', 'timing-tcp-connect')
|
||||||
|
Tracker.alias(safe_unicode, 'srt', 'timingServerResponse', 'timing-server-response')
|
||||||
|
|
||||||
|
# Custom dimensions and metrics
|
||||||
|
for i in range(0, 200):
|
||||||
|
Tracker.alias(safe_unicode, 'cd{0}'.format(i), 'dimension{0}'.format(i))
|
||||||
|
Tracker.alias(int, 'cm{0}'.format(i), 'metric{0}'.format(i))
|
||||||
|
|
||||||
|
# Content groups
|
||||||
|
for i in range(0, 5):
|
||||||
|
Tracker.alias(safe_unicode, 'cg{0}'.format(i), 'contentGroup{0}'.format(i))
|
||||||
|
|
||||||
|
# Enhanced Ecommerce
|
||||||
|
Tracker.alias(str, 'pa') # Product action
|
||||||
|
Tracker.alias(str, 'tcc') # Coupon code
|
||||||
|
Tracker.alias(unicode, 'pal') # Product action list
|
||||||
|
Tracker.alias(int, 'cos') # Checkout step
|
||||||
|
Tracker.alias(str, 'col') # Checkout step option
|
||||||
|
|
||||||
|
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}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
|
||||||
|
Tracker.alias(int, 'pr{0}ps'.format(product_index)) # Product position
|
||||||
|
|
||||||
|
for custom_index in range(MAX_CUSTOM_DEFINITIONS):
|
||||||
|
Tracker.alias(str, 'pr{0}cd{1}'.format(product_index, custom_index)) # Product custom dimension
|
||||||
|
Tracker.alias(int, 'pr{0}cm{1}'.format(product_index, custom_index)) # Product custom metric
|
||||||
|
|
||||||
|
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(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
|
||||||
|
|
||||||
|
for custom_index in range(MAX_CUSTOM_DEFINITIONS):
|
||||||
|
Tracker.alias(str, 'il{0}pi{1}cd{2}'.format(list_index, product_index,
|
||||||
|
custom_index)) # Product impression custom dimension
|
||||||
|
Tracker.alias(int, 'il{0}pi{1}cm{2}'.format(list_index, product_index,
|
||||||
|
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
|
||||||
|
|
||||||
|
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}cr'.format(promotion_index)) # Promotion creative
|
||||||
|
Tracker.alias(str, 'promo{0}ps'.format(promotion_index)) # Promotion position
|
||||||
|
|
||||||
|
|
||||||
|
# Shortcut for creating trackers
|
||||||
|
def create(account, *args, **kwargs):
|
||||||
|
return Tracker(account, *args, **kwargs)
|
||||||
|
|
||||||
|
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4
|
||||||
1
lib/UniversalAnalytics/__init__.py
Normal file
1
lib/UniversalAnalytics/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import Tracker
|
||||||
@@ -45,7 +45,8 @@ __version__ = version.__version__
|
|||||||
|
|
||||||
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
|
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
|
||||||
FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?"
|
FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?"
|
||||||
VALID_API_VERSIONS = ["2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9"]
|
VALID_API_VERSIONS = [
|
||||||
|
"2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12"]
|
||||||
VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"]
|
VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"]
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ class GraphAPI(object):
|
|||||||
self.session = session or requests.Session()
|
self.session = session or requests.Session()
|
||||||
|
|
||||||
if version:
|
if version:
|
||||||
version_regex = re.compile("^\d\.\d$")
|
version_regex = re.compile("^\d\.\d{1,2}$")
|
||||||
match = version_regex.search(str(version))
|
match = version_regex.search(str(version))
|
||||||
if match is not None:
|
if match is not None:
|
||||||
if str(version) not in VALID_API_VERSIONS:
|
if str(version) not in VALID_API_VERSIONS:
|
||||||
@@ -229,7 +230,7 @@ class GraphAPI(object):
|
|||||||
try:
|
try:
|
||||||
headers = response.headers
|
headers = response.headers
|
||||||
version = headers["facebook-api-version"].replace("v", "")
|
version = headers["facebook-api-version"].replace("v", "")
|
||||||
return float(version)
|
return str(version)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise GraphAPIError("API version number not available")
|
raise GraphAPIError("API version number not available")
|
||||||
|
|
||||||
@@ -369,24 +370,24 @@ class GraphAPIError(Exception):
|
|||||||
self.code = None
|
self.code = None
|
||||||
try:
|
try:
|
||||||
self.type = result["error_code"]
|
self.type = result["error_code"]
|
||||||
except:
|
except (KeyError, TypeError):
|
||||||
self.type = ""
|
self.type = ""
|
||||||
|
|
||||||
# OAuth 2.0 Draft 10
|
# OAuth 2.0 Draft 10
|
||||||
try:
|
try:
|
||||||
self.message = result["error_description"]
|
self.message = result["error_description"]
|
||||||
except:
|
except (KeyError, TypeError):
|
||||||
# OAuth 2.0 Draft 00
|
# OAuth 2.0 Draft 00
|
||||||
try:
|
try:
|
||||||
self.message = result["error"]["message"]
|
self.message = result["error"]["message"]
|
||||||
self.code = result["error"].get("code")
|
self.code = result["error"].get("code")
|
||||||
if not self.type:
|
if not self.type:
|
||||||
self.type = result["error"].get("type", "")
|
self.type = result["error"].get("type", "")
|
||||||
except:
|
except (KeyError, TypeError):
|
||||||
# REST server style
|
# REST server style
|
||||||
try:
|
try:
|
||||||
self.message = result["error_msg"]
|
self.message = result["error_msg"]
|
||||||
except:
|
except (KeyError, TypeError):
|
||||||
self.message = result
|
self.message = result
|
||||||
|
|
||||||
Exception.__init__(self, self.message)
|
Exception.__init__(self, self.message)
|
||||||
|
|||||||
29
lib/jwt/__init__.py
Normal file
29
lib/jwt/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# flake8: noqa
|
||||||
|
|
||||||
|
"""
|
||||||
|
JSON Web Token implementation
|
||||||
|
|
||||||
|
Minimum implementation based on this spec:
|
||||||
|
http://self-issued.info/docs/draft-jones-json-web-token-01.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__title__ = 'pyjwt'
|
||||||
|
__version__ = '1.4.0'
|
||||||
|
__author__ = 'José Padilla'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__copyright__ = 'Copyright 2015 José Padilla'
|
||||||
|
|
||||||
|
|
||||||
|
from .api_jwt import (
|
||||||
|
encode, decode, register_algorithm, unregister_algorithm,
|
||||||
|
get_unverified_header, PyJWT
|
||||||
|
)
|
||||||
|
from .api_jws import PyJWS
|
||||||
|
from .exceptions import (
|
||||||
|
InvalidTokenError, DecodeError, InvalidAudienceError,
|
||||||
|
ExpiredSignatureError, ImmatureSignatureError, InvalidIssuedAtError,
|
||||||
|
InvalidIssuerError, ExpiredSignature, InvalidAudience, InvalidIssuer,
|
||||||
|
MissingRequiredClaimError
|
||||||
|
)
|
||||||
135
lib/jwt/__main__.py
Normal file
135
lib/jwt/__main__.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import absolute_import, print_function
|
||||||
|
|
||||||
|
import json
|
||||||
|
import optparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from . import DecodeError, __package__, __version__, decode, encode
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
usage = '''Encodes or decodes JSON Web Tokens based on input.
|
||||||
|
|
||||||
|
%prog [options] input
|
||||||
|
|
||||||
|
Decoding examples:
|
||||||
|
|
||||||
|
%prog --key=secret json.web.token
|
||||||
|
%prog --no-verify json.web.token
|
||||||
|
|
||||||
|
Encoding requires the key option and takes space separated key/value pairs
|
||||||
|
separated by equals (=) as input. Examples:
|
||||||
|
|
||||||
|
%prog --key=secret iss=me exp=1302049071
|
||||||
|
%prog --key=secret foo=bar exp=+10
|
||||||
|
|
||||||
|
The exp key is special and can take an offset to current Unix time.\
|
||||||
|
'''
|
||||||
|
p = optparse.OptionParser(
|
||||||
|
usage=usage,
|
||||||
|
prog=__package__,
|
||||||
|
version='%s %s' % (__package__, __version__),
|
||||||
|
)
|
||||||
|
|
||||||
|
p.add_option(
|
||||||
|
'-n', '--no-verify',
|
||||||
|
action='store_false',
|
||||||
|
dest='verify',
|
||||||
|
default=True,
|
||||||
|
help='ignore signature verification on decode'
|
||||||
|
)
|
||||||
|
|
||||||
|
p.add_option(
|
||||||
|
'--key',
|
||||||
|
dest='key',
|
||||||
|
metavar='KEY',
|
||||||
|
default=None,
|
||||||
|
help='set the secret key to sign with'
|
||||||
|
)
|
||||||
|
|
||||||
|
p.add_option(
|
||||||
|
'--alg',
|
||||||
|
dest='algorithm',
|
||||||
|
metavar='ALG',
|
||||||
|
default='HS256',
|
||||||
|
help='set crypto algorithm to sign with. default=HS256'
|
||||||
|
)
|
||||||
|
|
||||||
|
options, arguments = p.parse_args()
|
||||||
|
|
||||||
|
if len(arguments) > 0 or not sys.stdin.isatty():
|
||||||
|
if len(arguments) == 1 and (not options.verify or options.key):
|
||||||
|
# Try to decode
|
||||||
|
try:
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
token = sys.stdin.read()
|
||||||
|
else:
|
||||||
|
token = arguments[0]
|
||||||
|
|
||||||
|
token = token.encode('utf-8')
|
||||||
|
data = decode(token, key=options.key, verify=options.verify)
|
||||||
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
sys.exit(0)
|
||||||
|
except DecodeError as e:
|
||||||
|
print(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Try to encode
|
||||||
|
if options.key is None:
|
||||||
|
print('Key is required when encoding. See --help for usage.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Build payload object to encode
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
for arg in arguments:
|
||||||
|
try:
|
||||||
|
k, v = arg.split('=', 1)
|
||||||
|
|
||||||
|
# exp +offset special case?
|
||||||
|
if k == 'exp' and v[0] == '+' and len(v) > 1:
|
||||||
|
v = str(int(time.time()+int(v[1:])))
|
||||||
|
|
||||||
|
# Cast to integer?
|
||||||
|
if v.isdigit():
|
||||||
|
v = int(v)
|
||||||
|
else:
|
||||||
|
# Cast to float?
|
||||||
|
try:
|
||||||
|
v = float(v)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Cast to true, false, or null?
|
||||||
|
constants = {'true': True, 'false': False, 'null': None}
|
||||||
|
|
||||||
|
if v in constants:
|
||||||
|
v = constants[v]
|
||||||
|
|
||||||
|
payload[k] = v
|
||||||
|
except ValueError:
|
||||||
|
print('Invalid encoding input at {}'.format(arg))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = encode(
|
||||||
|
payload,
|
||||||
|
key=options.key,
|
||||||
|
algorithm=options.algorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
print(token)
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
p.print_help()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
290
lib/jwt/algorithms.py
Normal file
290
lib/jwt/algorithms.py
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
from .compat import constant_time_compare, string_types, text_type
|
||||||
|
from .exceptions import InvalidKeyError
|
||||||
|
from .utils import der_to_raw_signature, raw_to_der_signature
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.serialization import (
|
||||||
|
load_pem_private_key, load_pem_public_key, load_ssh_public_key
|
||||||
|
)
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||||
|
RSAPrivateKey, RSAPublicKey
|
||||||
|
)
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||||
|
EllipticCurvePrivateKey, EllipticCurvePublicKey
|
||||||
|
)
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec, padding
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
|
||||||
|
has_crypto = True
|
||||||
|
except ImportError:
|
||||||
|
has_crypto = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_algorithms():
|
||||||
|
"""
|
||||||
|
Returns the algorithms that are implemented by the library.
|
||||||
|
"""
|
||||||
|
default_algorithms = {
|
||||||
|
'none': NoneAlgorithm(),
|
||||||
|
'HS256': HMACAlgorithm(HMACAlgorithm.SHA256),
|
||||||
|
'HS384': HMACAlgorithm(HMACAlgorithm.SHA384),
|
||||||
|
'HS512': HMACAlgorithm(HMACAlgorithm.SHA512)
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_crypto:
|
||||||
|
default_algorithms.update({
|
||||||
|
'RS256': RSAAlgorithm(RSAAlgorithm.SHA256),
|
||||||
|
'RS384': RSAAlgorithm(RSAAlgorithm.SHA384),
|
||||||
|
'RS512': RSAAlgorithm(RSAAlgorithm.SHA512),
|
||||||
|
'ES256': ECAlgorithm(ECAlgorithm.SHA256),
|
||||||
|
'ES384': ECAlgorithm(ECAlgorithm.SHA384),
|
||||||
|
'ES512': ECAlgorithm(ECAlgorithm.SHA512),
|
||||||
|
'PS256': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256),
|
||||||
|
'PS384': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384),
|
||||||
|
'PS512': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512)
|
||||||
|
})
|
||||||
|
|
||||||
|
return default_algorithms
|
||||||
|
|
||||||
|
|
||||||
|
class Algorithm(object):
|
||||||
|
"""
|
||||||
|
The interface for an algorithm used to sign and verify tokens.
|
||||||
|
"""
|
||||||
|
def prepare_key(self, key):
|
||||||
|
"""
|
||||||
|
Performs necessary validation and conversions on the key and returns
|
||||||
|
the key value in the proper format for sign() and verify().
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
"""
|
||||||
|
Returns a digital signature for the specified message
|
||||||
|
using the specified key value.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
"""
|
||||||
|
Verifies that the specified digital signature is valid
|
||||||
|
for the specified message and key values.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class NoneAlgorithm(Algorithm):
|
||||||
|
"""
|
||||||
|
Placeholder for use when no signing or verification
|
||||||
|
operations are required.
|
||||||
|
"""
|
||||||
|
def prepare_key(self, key):
|
||||||
|
if key == '':
|
||||||
|
key = None
|
||||||
|
|
||||||
|
if key is not None:
|
||||||
|
raise InvalidKeyError('When alg = "none", key value must be None.')
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
return b''
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class HMACAlgorithm(Algorithm):
|
||||||
|
"""
|
||||||
|
Performs signing and verification operations using HMAC
|
||||||
|
and the specified hash function.
|
||||||
|
"""
|
||||||
|
SHA256 = hashlib.sha256
|
||||||
|
SHA384 = hashlib.sha384
|
||||||
|
SHA512 = hashlib.sha512
|
||||||
|
|
||||||
|
def __init__(self, hash_alg):
|
||||||
|
self.hash_alg = hash_alg
|
||||||
|
|
||||||
|
def prepare_key(self, key):
|
||||||
|
if not isinstance(key, string_types) and not isinstance(key, bytes):
|
||||||
|
raise TypeError('Expecting a string- or bytes-formatted key.')
|
||||||
|
|
||||||
|
if isinstance(key, text_type):
|
||||||
|
key = key.encode('utf-8')
|
||||||
|
|
||||||
|
invalid_strings = [
|
||||||
|
b'-----BEGIN PUBLIC KEY-----',
|
||||||
|
b'-----BEGIN CERTIFICATE-----',
|
||||||
|
b'ssh-rsa'
|
||||||
|
]
|
||||||
|
|
||||||
|
if any([string_value in key for string_value in invalid_strings]):
|
||||||
|
raise InvalidKeyError(
|
||||||
|
'The specified key is an asymmetric key or x509 certificate and'
|
||||||
|
' should not be used as an HMAC secret.')
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
return hmac.new(key, msg, self.hash_alg).digest()
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
return constant_time_compare(sig, self.sign(msg, key))
|
||||||
|
|
||||||
|
if has_crypto:
|
||||||
|
|
||||||
|
class RSAAlgorithm(Algorithm):
|
||||||
|
"""
|
||||||
|
Performs signing and verification operations using
|
||||||
|
RSASSA-PKCS-v1_5 and the specified hash function.
|
||||||
|
"""
|
||||||
|
SHA256 = hashes.SHA256
|
||||||
|
SHA384 = hashes.SHA384
|
||||||
|
SHA512 = hashes.SHA512
|
||||||
|
|
||||||
|
def __init__(self, hash_alg):
|
||||||
|
self.hash_alg = hash_alg
|
||||||
|
|
||||||
|
def prepare_key(self, key):
|
||||||
|
if isinstance(key, RSAPrivateKey) or \
|
||||||
|
isinstance(key, RSAPublicKey):
|
||||||
|
return key
|
||||||
|
|
||||||
|
if isinstance(key, string_types):
|
||||||
|
if isinstance(key, text_type):
|
||||||
|
key = key.encode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if key.startswith(b'ssh-rsa'):
|
||||||
|
key = load_ssh_public_key(key, backend=default_backend())
|
||||||
|
else:
|
||||||
|
key = load_pem_private_key(key, password=None, backend=default_backend())
|
||||||
|
except ValueError:
|
||||||
|
key = load_pem_public_key(key, backend=default_backend())
|
||||||
|
else:
|
||||||
|
raise TypeError('Expecting a PEM-formatted key.')
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
signer = key.signer(
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
self.hash_alg()
|
||||||
|
)
|
||||||
|
|
||||||
|
signer.update(msg)
|
||||||
|
return signer.finalize()
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
verifier = key.verifier(
|
||||||
|
sig,
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
self.hash_alg()
|
||||||
|
)
|
||||||
|
|
||||||
|
verifier.update(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
verifier.verify()
|
||||||
|
return True
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class ECAlgorithm(Algorithm):
|
||||||
|
"""
|
||||||
|
Performs signing and verification operations using
|
||||||
|
ECDSA and the specified hash function
|
||||||
|
"""
|
||||||
|
SHA256 = hashes.SHA256
|
||||||
|
SHA384 = hashes.SHA384
|
||||||
|
SHA512 = hashes.SHA512
|
||||||
|
|
||||||
|
def __init__(self, hash_alg):
|
||||||
|
self.hash_alg = hash_alg
|
||||||
|
|
||||||
|
def prepare_key(self, key):
|
||||||
|
if isinstance(key, EllipticCurvePrivateKey) or \
|
||||||
|
isinstance(key, EllipticCurvePublicKey):
|
||||||
|
return key
|
||||||
|
|
||||||
|
if isinstance(key, string_types):
|
||||||
|
if isinstance(key, text_type):
|
||||||
|
key = key.encode('utf-8')
|
||||||
|
|
||||||
|
# Attempt to load key. We don't know if it's
|
||||||
|
# a Signing Key or a Verifying Key, so we try
|
||||||
|
# the Verifying Key first.
|
||||||
|
try:
|
||||||
|
key = load_pem_public_key(key, backend=default_backend())
|
||||||
|
except ValueError:
|
||||||
|
key = load_pem_private_key(key, password=None, backend=default_backend())
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError('Expecting a PEM-formatted key.')
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
signer = key.signer(ec.ECDSA(self.hash_alg()))
|
||||||
|
|
||||||
|
signer.update(msg)
|
||||||
|
der_sig = signer.finalize()
|
||||||
|
|
||||||
|
return der_to_raw_signature(der_sig, key.curve)
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
try:
|
||||||
|
der_sig = raw_to_der_signature(sig, key.curve)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
verifier = key.verifier(der_sig, ec.ECDSA(self.hash_alg()))
|
||||||
|
|
||||||
|
verifier.update(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
verifier.verify()
|
||||||
|
return True
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class RSAPSSAlgorithm(RSAAlgorithm):
|
||||||
|
"""
|
||||||
|
Performs a signature using RSASSA-PSS with MGF1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
signer = key.signer(
|
||||||
|
padding.PSS(
|
||||||
|
mgf=padding.MGF1(self.hash_alg()),
|
||||||
|
salt_length=self.hash_alg.digest_size
|
||||||
|
),
|
||||||
|
self.hash_alg()
|
||||||
|
)
|
||||||
|
|
||||||
|
signer.update(msg)
|
||||||
|
return signer.finalize()
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
verifier = key.verifier(
|
||||||
|
sig,
|
||||||
|
padding.PSS(
|
||||||
|
mgf=padding.MGF1(self.hash_alg()),
|
||||||
|
salt_length=self.hash_alg.digest_size
|
||||||
|
),
|
||||||
|
self.hash_alg()
|
||||||
|
)
|
||||||
|
|
||||||
|
verifier.update(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
verifier.verify()
|
||||||
|
return True
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
189
lib/jwt/api_jws.py
Normal file
189
lib/jwt/api_jws.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from collections import Mapping
|
||||||
|
|
||||||
|
from .algorithms import Algorithm, get_default_algorithms # NOQA
|
||||||
|
from .compat import text_type
|
||||||
|
from .exceptions import DecodeError, InvalidAlgorithmError
|
||||||
|
from .utils import base64url_decode, base64url_encode, merge_dict
|
||||||
|
|
||||||
|
|
||||||
|
class PyJWS(object):
|
||||||
|
header_typ = 'JWT'
|
||||||
|
|
||||||
|
def __init__(self, algorithms=None, options=None):
|
||||||
|
self._algorithms = get_default_algorithms()
|
||||||
|
self._valid_algs = (set(algorithms) if algorithms is not None
|
||||||
|
else set(self._algorithms))
|
||||||
|
|
||||||
|
# Remove algorithms that aren't on the whitelist
|
||||||
|
for key in list(self._algorithms.keys()):
|
||||||
|
if key not in self._valid_algs:
|
||||||
|
del self._algorithms[key]
|
||||||
|
|
||||||
|
if not options:
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
self.options = merge_dict(self._get_default_options(), options)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_default_options():
|
||||||
|
return {
|
||||||
|
'verify_signature': True
|
||||||
|
}
|
||||||
|
|
||||||
|
def register_algorithm(self, alg_id, alg_obj):
|
||||||
|
"""
|
||||||
|
Registers a new Algorithm for use when creating and verifying tokens.
|
||||||
|
"""
|
||||||
|
if alg_id in self._algorithms:
|
||||||
|
raise ValueError('Algorithm already has a handler.')
|
||||||
|
|
||||||
|
if not isinstance(alg_obj, Algorithm):
|
||||||
|
raise TypeError('Object is not of type `Algorithm`')
|
||||||
|
|
||||||
|
self._algorithms[alg_id] = alg_obj
|
||||||
|
self._valid_algs.add(alg_id)
|
||||||
|
|
||||||
|
def unregister_algorithm(self, alg_id):
|
||||||
|
"""
|
||||||
|
Unregisters an Algorithm for use when creating and verifying tokens
|
||||||
|
Throws KeyError if algorithm is not registered.
|
||||||
|
"""
|
||||||
|
if alg_id not in self._algorithms:
|
||||||
|
raise KeyError('The specified algorithm could not be removed'
|
||||||
|
' because it is not registered.')
|
||||||
|
|
||||||
|
del self._algorithms[alg_id]
|
||||||
|
self._valid_algs.remove(alg_id)
|
||||||
|
|
||||||
|
def get_algorithms(self):
|
||||||
|
"""
|
||||||
|
Returns a list of supported values for the 'alg' parameter.
|
||||||
|
"""
|
||||||
|
return list(self._valid_algs)
|
||||||
|
|
||||||
|
def encode(self, payload, key, algorithm='HS256', headers=None,
|
||||||
|
json_encoder=None):
|
||||||
|
segments = []
|
||||||
|
|
||||||
|
if algorithm is None:
|
||||||
|
algorithm = 'none'
|
||||||
|
|
||||||
|
if algorithm not in self._valid_algs:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header = {'typ': self.header_typ, 'alg': algorithm}
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
header.update(headers)
|
||||||
|
|
||||||
|
json_header = json.dumps(
|
||||||
|
header,
|
||||||
|
separators=(',', ':'),
|
||||||
|
cls=json_encoder
|
||||||
|
).encode('utf-8')
|
||||||
|
|
||||||
|
segments.append(base64url_encode(json_header))
|
||||||
|
segments.append(base64url_encode(payload))
|
||||||
|
|
||||||
|
# Segments
|
||||||
|
signing_input = b'.'.join(segments)
|
||||||
|
try:
|
||||||
|
alg_obj = self._algorithms[algorithm]
|
||||||
|
key = alg_obj.prepare_key(key)
|
||||||
|
signature = alg_obj.sign(signing_input, key)
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
raise NotImplementedError('Algorithm not supported')
|
||||||
|
|
||||||
|
segments.append(base64url_encode(signature))
|
||||||
|
|
||||||
|
return b'.'.join(segments)
|
||||||
|
|
||||||
|
def decode(self, jws, key='', verify=True, algorithms=None, options=None,
|
||||||
|
**kwargs):
|
||||||
|
payload, signing_input, header, signature = self._load(jws)
|
||||||
|
|
||||||
|
if verify:
|
||||||
|
merged_options = merge_dict(self.options, options)
|
||||||
|
if merged_options.get('verify_signature'):
|
||||||
|
self._verify_signature(payload, signing_input, header, signature,
|
||||||
|
key, algorithms)
|
||||||
|
else:
|
||||||
|
warnings.warn('The verify parameter is deprecated. '
|
||||||
|
'Please use options instead.', DeprecationWarning)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def get_unverified_header(self, jwt):
|
||||||
|
"""Returns back the JWT header parameters as a dict()
|
||||||
|
|
||||||
|
Note: The signature is not verified so the header parameters
|
||||||
|
should not be fully trusted until signature verification is complete
|
||||||
|
"""
|
||||||
|
return self._load(jwt)[2]
|
||||||
|
|
||||||
|
def _load(self, jwt):
|
||||||
|
if isinstance(jwt, text_type):
|
||||||
|
jwt = jwt.encode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
signing_input, crypto_segment = jwt.rsplit(b'.', 1)
|
||||||
|
header_segment, payload_segment = signing_input.split(b'.', 1)
|
||||||
|
except ValueError:
|
||||||
|
raise DecodeError('Not enough segments')
|
||||||
|
|
||||||
|
try:
|
||||||
|
header_data = base64url_decode(header_segment)
|
||||||
|
except (TypeError, binascii.Error):
|
||||||
|
raise DecodeError('Invalid header padding')
|
||||||
|
|
||||||
|
try:
|
||||||
|
header = json.loads(header_data.decode('utf-8'))
|
||||||
|
except ValueError as e:
|
||||||
|
raise DecodeError('Invalid header string: %s' % e)
|
||||||
|
|
||||||
|
if not isinstance(header, Mapping):
|
||||||
|
raise DecodeError('Invalid header string: must be a json object')
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = base64url_decode(payload_segment)
|
||||||
|
except (TypeError, binascii.Error):
|
||||||
|
raise DecodeError('Invalid payload padding')
|
||||||
|
|
||||||
|
try:
|
||||||
|
signature = base64url_decode(crypto_segment)
|
||||||
|
except (TypeError, binascii.Error):
|
||||||
|
raise DecodeError('Invalid crypto padding')
|
||||||
|
|
||||||
|
return (payload, signing_input, header, signature)
|
||||||
|
|
||||||
|
def _verify_signature(self, payload, signing_input, header, signature,
|
||||||
|
key='', algorithms=None):
|
||||||
|
|
||||||
|
alg = header.get('alg')
|
||||||
|
|
||||||
|
if algorithms is not None and alg not in algorithms:
|
||||||
|
raise InvalidAlgorithmError('The specified alg value is not allowed')
|
||||||
|
|
||||||
|
try:
|
||||||
|
alg_obj = self._algorithms[alg]
|
||||||
|
key = alg_obj.prepare_key(key)
|
||||||
|
|
||||||
|
if not alg_obj.verify(signing_input, key, signature):
|
||||||
|
raise DecodeError('Signature verification failed')
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
raise InvalidAlgorithmError('Algorithm not supported')
|
||||||
|
|
||||||
|
|
||||||
|
_jws_global_obj = PyJWS()
|
||||||
|
encode = _jws_global_obj.encode
|
||||||
|
decode = _jws_global_obj.decode
|
||||||
|
register_algorithm = _jws_global_obj.register_algorithm
|
||||||
|
unregister_algorithm = _jws_global_obj.unregister_algorithm
|
||||||
|
get_unverified_header = _jws_global_obj.get_unverified_header
|
||||||
187
lib/jwt/api_jwt.py
Normal file
187
lib/jwt/api_jwt.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import json
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from calendar import timegm
|
||||||
|
from collections import Mapping
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from .api_jws import PyJWS
|
||||||
|
from .algorithms import Algorithm, get_default_algorithms # NOQA
|
||||||
|
from .compat import string_types, timedelta_total_seconds
|
||||||
|
from .exceptions import (
|
||||||
|
DecodeError, ExpiredSignatureError, ImmatureSignatureError,
|
||||||
|
InvalidAudienceError, InvalidIssuedAtError,
|
||||||
|
InvalidIssuerError, MissingRequiredClaimError
|
||||||
|
)
|
||||||
|
from .utils import merge_dict
|
||||||
|
|
||||||
|
|
||||||
|
class PyJWT(PyJWS):
|
||||||
|
header_type = 'JWT'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_default_options():
|
||||||
|
return {
|
||||||
|
'verify_signature': True,
|
||||||
|
'verify_exp': True,
|
||||||
|
'verify_nbf': True,
|
||||||
|
'verify_iat': True,
|
||||||
|
'verify_aud': True,
|
||||||
|
'verify_iss': True,
|
||||||
|
'require_exp': False,
|
||||||
|
'require_iat': False,
|
||||||
|
'require_nbf': False
|
||||||
|
}
|
||||||
|
|
||||||
|
def encode(self, payload, key, algorithm='HS256', headers=None,
|
||||||
|
json_encoder=None):
|
||||||
|
# Check that we get a mapping
|
||||||
|
if not isinstance(payload, Mapping):
|
||||||
|
raise TypeError('Expecting a mapping object, as JWT only supports '
|
||||||
|
'JSON objects as payloads.')
|
||||||
|
|
||||||
|
# Payload
|
||||||
|
for time_claim in ['exp', 'iat', 'nbf']:
|
||||||
|
# Convert datetime to a intDate value in known time-format claims
|
||||||
|
if isinstance(payload.get(time_claim), datetime):
|
||||||
|
payload[time_claim] = timegm(payload[time_claim].utctimetuple())
|
||||||
|
|
||||||
|
json_payload = json.dumps(
|
||||||
|
payload,
|
||||||
|
separators=(',', ':'),
|
||||||
|
cls=json_encoder
|
||||||
|
).encode('utf-8')
|
||||||
|
|
||||||
|
return super(PyJWT, self).encode(
|
||||||
|
json_payload, key, algorithm, headers, json_encoder
|
||||||
|
)
|
||||||
|
|
||||||
|
def decode(self, jwt, key='', verify=True, algorithms=None, options=None,
|
||||||
|
**kwargs):
|
||||||
|
payload, signing_input, header, signature = self._load(jwt)
|
||||||
|
|
||||||
|
decoded = super(PyJWT, self).decode(jwt, key, verify, algorithms,
|
||||||
|
options, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(decoded.decode('utf-8'))
|
||||||
|
except ValueError as e:
|
||||||
|
raise DecodeError('Invalid payload string: %s' % e)
|
||||||
|
if not isinstance(payload, Mapping):
|
||||||
|
raise DecodeError('Invalid payload string: must be a json object')
|
||||||
|
|
||||||
|
if verify:
|
||||||
|
merged_options = merge_dict(self.options, options)
|
||||||
|
self._validate_claims(payload, merged_options, **kwargs)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _validate_claims(self, payload, options, audience=None, issuer=None,
|
||||||
|
leeway=0, **kwargs):
|
||||||
|
|
||||||
|
if 'verify_expiration' in kwargs:
|
||||||
|
options['verify_exp'] = kwargs.get('verify_expiration', True)
|
||||||
|
warnings.warn('The verify_expiration parameter is deprecated. '
|
||||||
|
'Please use options instead.', DeprecationWarning)
|
||||||
|
|
||||||
|
if isinstance(leeway, timedelta):
|
||||||
|
leeway = timedelta_total_seconds(leeway)
|
||||||
|
|
||||||
|
if not isinstance(audience, (string_types, type(None))):
|
||||||
|
raise TypeError('audience must be a string or None')
|
||||||
|
|
||||||
|
self._validate_required_claims(payload, options)
|
||||||
|
|
||||||
|
now = timegm(datetime.utcnow().utctimetuple())
|
||||||
|
|
||||||
|
if 'iat' in payload and options.get('verify_iat'):
|
||||||
|
self._validate_iat(payload, now, leeway)
|
||||||
|
|
||||||
|
if 'nbf' in payload and options.get('verify_nbf'):
|
||||||
|
self._validate_nbf(payload, now, leeway)
|
||||||
|
|
||||||
|
if 'exp' in payload and options.get('verify_exp'):
|
||||||
|
self._validate_exp(payload, now, leeway)
|
||||||
|
|
||||||
|
if options.get('verify_iss'):
|
||||||
|
self._validate_iss(payload, issuer)
|
||||||
|
|
||||||
|
if options.get('verify_aud'):
|
||||||
|
self._validate_aud(payload, audience)
|
||||||
|
|
||||||
|
def _validate_required_claims(self, payload, options):
|
||||||
|
if options.get('require_exp') and payload.get('exp') is None:
|
||||||
|
raise MissingRequiredClaimError('exp')
|
||||||
|
|
||||||
|
if options.get('require_iat') and payload.get('iat') is None:
|
||||||
|
raise MissingRequiredClaimError('iat')
|
||||||
|
|
||||||
|
if options.get('require_nbf') and payload.get('nbf') is None:
|
||||||
|
raise MissingRequiredClaimError('nbf')
|
||||||
|
|
||||||
|
def _validate_iat(self, payload, now, leeway):
|
||||||
|
try:
|
||||||
|
iat = int(payload['iat'])
|
||||||
|
except ValueError:
|
||||||
|
raise DecodeError('Issued At claim (iat) must be an integer.')
|
||||||
|
|
||||||
|
if iat > (now + leeway):
|
||||||
|
raise InvalidIssuedAtError('Issued At claim (iat) cannot be in'
|
||||||
|
' the future.')
|
||||||
|
|
||||||
|
def _validate_nbf(self, payload, now, leeway):
|
||||||
|
try:
|
||||||
|
nbf = int(payload['nbf'])
|
||||||
|
except ValueError:
|
||||||
|
raise DecodeError('Not Before claim (nbf) must be an integer.')
|
||||||
|
|
||||||
|
if nbf > (now + leeway):
|
||||||
|
raise ImmatureSignatureError('The token is not yet valid (nbf)')
|
||||||
|
|
||||||
|
def _validate_exp(self, payload, now, leeway):
|
||||||
|
try:
|
||||||
|
exp = int(payload['exp'])
|
||||||
|
except ValueError:
|
||||||
|
raise DecodeError('Expiration Time claim (exp) must be an'
|
||||||
|
' integer.')
|
||||||
|
|
||||||
|
if exp < (now - leeway):
|
||||||
|
raise ExpiredSignatureError('Signature has expired')
|
||||||
|
|
||||||
|
def _validate_aud(self, payload, audience):
|
||||||
|
if audience is None and 'aud' not in payload:
|
||||||
|
return
|
||||||
|
|
||||||
|
if audience is not None and 'aud' not in payload:
|
||||||
|
# Application specified an audience, but it could not be
|
||||||
|
# verified since the token does not contain a claim.
|
||||||
|
raise MissingRequiredClaimError('aud')
|
||||||
|
|
||||||
|
audience_claims = payload['aud']
|
||||||
|
|
||||||
|
if isinstance(audience_claims, string_types):
|
||||||
|
audience_claims = [audience_claims]
|
||||||
|
if not isinstance(audience_claims, list):
|
||||||
|
raise InvalidAudienceError('Invalid claim format in token')
|
||||||
|
if any(not isinstance(c, string_types) for c in audience_claims):
|
||||||
|
raise InvalidAudienceError('Invalid claim format in token')
|
||||||
|
if audience not in audience_claims:
|
||||||
|
raise InvalidAudienceError('Invalid audience')
|
||||||
|
|
||||||
|
def _validate_iss(self, payload, issuer):
|
||||||
|
if issuer is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'iss' not in payload:
|
||||||
|
raise MissingRequiredClaimError('iss')
|
||||||
|
|
||||||
|
if payload['iss'] != issuer:
|
||||||
|
raise InvalidIssuerError('Invalid issuer')
|
||||||
|
|
||||||
|
|
||||||
|
_jwt_global_obj = PyJWT()
|
||||||
|
encode = _jwt_global_obj.encode
|
||||||
|
decode = _jwt_global_obj.decode
|
||||||
|
register_algorithm = _jwt_global_obj.register_algorithm
|
||||||
|
unregister_algorithm = _jwt_global_obj.unregister_algorithm
|
||||||
|
get_unverified_header = _jwt_global_obj.get_unverified_header
|
||||||
52
lib/jwt/compat.py
Normal file
52
lib/jwt/compat.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
The `compat` module provides support for backwards compatibility with older
|
||||||
|
versions of python, and compatibility wrappers around optional packages.
|
||||||
|
"""
|
||||||
|
# flake8: noqa
|
||||||
|
import sys
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
string_types = str,
|
||||||
|
text_type = str
|
||||||
|
else:
|
||||||
|
string_types = basestring,
|
||||||
|
text_type = unicode
|
||||||
|
|
||||||
|
|
||||||
|
def timedelta_total_seconds(delta):
|
||||||
|
try:
|
||||||
|
delta.total_seconds
|
||||||
|
except AttributeError:
|
||||||
|
# On Python 2.6, timedelta instances do not have
|
||||||
|
# a .total_seconds() method.
|
||||||
|
total_seconds = delta.days * 24 * 60 * 60 + delta.seconds
|
||||||
|
else:
|
||||||
|
total_seconds = delta.total_seconds()
|
||||||
|
|
||||||
|
return total_seconds
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
constant_time_compare = hmac.compare_digest
|
||||||
|
except AttributeError:
|
||||||
|
# Fallback for Python < 2.7
|
||||||
|
def constant_time_compare(val1, val2):
|
||||||
|
"""
|
||||||
|
Returns True if the two strings are equal, False otherwise.
|
||||||
|
|
||||||
|
The time taken is independent of the number of characters that match.
|
||||||
|
"""
|
||||||
|
if len(val1) != len(val2):
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
for x, y in zip(val1, val2):
|
||||||
|
result |= ord(x) ^ ord(y)
|
||||||
|
|
||||||
|
return result == 0
|
||||||
0
lib/jwt/contrib/__init__.py
Normal file
0
lib/jwt/contrib/__init__.py
Normal file
0
lib/jwt/contrib/algorithms/__init__.py
Normal file
0
lib/jwt/contrib/algorithms/__init__.py
Normal file
60
lib/jwt/contrib/algorithms/py_ecdsa.py
Normal file
60
lib/jwt/contrib/algorithms/py_ecdsa.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Note: This file is named py_ecdsa.py because import behavior in Python 2
|
||||||
|
# would cause ecdsa.py to squash the ecdsa library that it depends upon.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import ecdsa
|
||||||
|
|
||||||
|
from jwt.algorithms import Algorithm
|
||||||
|
from jwt.compat import string_types, text_type
|
||||||
|
|
||||||
|
|
||||||
|
class ECAlgorithm(Algorithm):
|
||||||
|
"""
|
||||||
|
Performs signing and verification operations using
|
||||||
|
ECDSA and the specified hash function
|
||||||
|
|
||||||
|
This class requires the ecdsa package to be installed.
|
||||||
|
|
||||||
|
This is based off of the implementation in PyJWT 0.3.2
|
||||||
|
"""
|
||||||
|
SHA256 = hashlib.sha256
|
||||||
|
SHA384 = hashlib.sha384
|
||||||
|
SHA512 = hashlib.sha512
|
||||||
|
|
||||||
|
def __init__(self, hash_alg):
|
||||||
|
self.hash_alg = hash_alg
|
||||||
|
|
||||||
|
def prepare_key(self, key):
|
||||||
|
|
||||||
|
if isinstance(key, ecdsa.SigningKey) or \
|
||||||
|
isinstance(key, ecdsa.VerifyingKey):
|
||||||
|
return key
|
||||||
|
|
||||||
|
if isinstance(key, string_types):
|
||||||
|
if isinstance(key, text_type):
|
||||||
|
key = key.encode('utf-8')
|
||||||
|
|
||||||
|
# Attempt to load key. We don't know if it's
|
||||||
|
# a Signing Key or a Verifying Key, so we try
|
||||||
|
# the Verifying Key first.
|
||||||
|
try:
|
||||||
|
key = ecdsa.VerifyingKey.from_pem(key)
|
||||||
|
except ecdsa.der.UnexpectedDER:
|
||||||
|
key = ecdsa.SigningKey.from_pem(key)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError('Expecting a PEM-formatted key.')
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
return key.sign(msg, hashfunc=self.hash_alg,
|
||||||
|
sigencode=ecdsa.util.sigencode_string)
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
try:
|
||||||
|
return key.verify(sig, msg, hashfunc=self.hash_alg,
|
||||||
|
sigdecode=ecdsa.util.sigdecode_string)
|
||||||
|
except AssertionError:
|
||||||
|
return False
|
||||||
47
lib/jwt/contrib/algorithms/pycrypto.py
Normal file
47
lib/jwt/contrib/algorithms/pycrypto.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Crypto.Hash.SHA256
|
||||||
|
import Crypto.Hash.SHA384
|
||||||
|
import Crypto.Hash.SHA512
|
||||||
|
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Signature import PKCS1_v1_5
|
||||||
|
|
||||||
|
from jwt.algorithms import Algorithm
|
||||||
|
from jwt.compat import string_types, text_type
|
||||||
|
|
||||||
|
|
||||||
|
class RSAAlgorithm(Algorithm):
|
||||||
|
"""
|
||||||
|
Performs signing and verification operations using
|
||||||
|
RSASSA-PKCS-v1_5 and the specified hash function.
|
||||||
|
|
||||||
|
This class requires PyCrypto package to be installed.
|
||||||
|
|
||||||
|
This is based off of the implementation in PyJWT 0.3.2
|
||||||
|
"""
|
||||||
|
SHA256 = Crypto.Hash.SHA256
|
||||||
|
SHA384 = Crypto.Hash.SHA384
|
||||||
|
SHA512 = Crypto.Hash.SHA512
|
||||||
|
|
||||||
|
def __init__(self, hash_alg):
|
||||||
|
self.hash_alg = hash_alg
|
||||||
|
|
||||||
|
def prepare_key(self, key):
|
||||||
|
|
||||||
|
if isinstance(key, RSA._RSAobj):
|
||||||
|
return key
|
||||||
|
|
||||||
|
if isinstance(key, string_types):
|
||||||
|
if isinstance(key, text_type):
|
||||||
|
key = key.encode('utf-8')
|
||||||
|
|
||||||
|
key = RSA.importKey(key)
|
||||||
|
else:
|
||||||
|
raise TypeError('Expecting a PEM- or RSA-formatted key.')
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def sign(self, msg, key):
|
||||||
|
return PKCS1_v1_5.new(key).sign(self.hash_alg.new(msg))
|
||||||
|
|
||||||
|
def verify(self, msg, key, sig):
|
||||||
|
return PKCS1_v1_5.new(key).verify(self.hash_alg.new(msg), sig)
|
||||||
48
lib/jwt/exceptions.py
Normal file
48
lib/jwt/exceptions.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
class InvalidTokenError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DecodeError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ExpiredSignatureError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAudienceError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIssuerError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIssuedAtError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ImmatureSignatureError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidKeyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAlgorithmError(InvalidTokenError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingRequiredClaimError(InvalidTokenError):
|
||||||
|
def __init__(self, claim):
|
||||||
|
self.claim = claim
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Token is missing the "%s" claim' % self.claim
|
||||||
|
|
||||||
|
|
||||||
|
# Compatibility aliases (deprecated)
|
||||||
|
ExpiredSignature = ExpiredSignatureError
|
||||||
|
InvalidAudience = InvalidAudienceError
|
||||||
|
InvalidIssuer = InvalidIssuerError
|
||||||
67
lib/jwt/utils.py
Normal file
67
lib/jwt/utils.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.utils import (
|
||||||
|
decode_rfc6979_signature, encode_rfc6979_signature
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def base64url_decode(input):
|
||||||
|
rem = len(input) % 4
|
||||||
|
|
||||||
|
if rem > 0:
|
||||||
|
input += b'=' * (4 - rem)
|
||||||
|
|
||||||
|
return base64.urlsafe_b64decode(input)
|
||||||
|
|
||||||
|
|
||||||
|
def base64url_encode(input):
|
||||||
|
return base64.urlsafe_b64encode(input).replace(b'=', b'')
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dict(original, updates):
|
||||||
|
if not updates:
|
||||||
|
return original
|
||||||
|
|
||||||
|
try:
|
||||||
|
merged_options = original.copy()
|
||||||
|
merged_options.update(updates)
|
||||||
|
except (AttributeError, ValueError) as e:
|
||||||
|
raise TypeError('original and updates must be a dictionary: %s' % e)
|
||||||
|
|
||||||
|
return merged_options
|
||||||
|
|
||||||
|
|
||||||
|
def number_to_bytes(num, num_bytes):
|
||||||
|
padded_hex = '%0*x' % (2 * num_bytes, num)
|
||||||
|
big_endian = binascii.a2b_hex(padded_hex.encode('ascii'))
|
||||||
|
return big_endian
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_number(string):
|
||||||
|
return int(binascii.b2a_hex(string), 16)
|
||||||
|
|
||||||
|
|
||||||
|
def der_to_raw_signature(der_sig, curve):
|
||||||
|
num_bits = curve.key_size
|
||||||
|
num_bytes = (num_bits + 7) // 8
|
||||||
|
|
||||||
|
r, s = decode_rfc6979_signature(der_sig)
|
||||||
|
|
||||||
|
return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def raw_to_der_signature(raw_sig, curve):
|
||||||
|
num_bits = curve.key_size
|
||||||
|
num_bytes = (num_bits + 7) // 8
|
||||||
|
|
||||||
|
if len(raw_sig) != 2 * num_bytes:
|
||||||
|
raise ValueError('Invalid signature')
|
||||||
|
|
||||||
|
r = bytes_to_number(raw_sig[:num_bytes])
|
||||||
|
s = bytes_to_number(raw_sig[num_bytes:])
|
||||||
|
|
||||||
|
return encode_rfc6979_signature(r, s)
|
||||||
@@ -21,6 +21,7 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Some cut down versions of Python may not include this module and it's not critical for us
|
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||||
try:
|
try:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -31,10 +32,11 @@ except ImportError:
|
|||||||
import cherrypy
|
import cherrypy
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
|
from UniversalAnalytics import Tracker
|
||||||
|
|
||||||
import activity_handler
|
import activity_handler
|
||||||
import activity_pinger
|
import activity_pinger
|
||||||
import config
|
import common
|
||||||
import database
|
import database
|
||||||
import libraries
|
import libraries
|
||||||
import logger
|
import logger
|
||||||
@@ -42,9 +44,9 @@ import mobile_app
|
|||||||
import notification_handler
|
import notification_handler
|
||||||
import notifiers
|
import notifiers
|
||||||
import plextv
|
import plextv
|
||||||
import pmsconnect
|
|
||||||
import users
|
import users
|
||||||
import versioncheck
|
import versioncheck
|
||||||
|
import web_socket
|
||||||
import plexpy.config
|
import plexpy.config
|
||||||
|
|
||||||
PROG_DIR = None
|
PROG_DIR = None
|
||||||
@@ -54,6 +56,7 @@ ARGS = None
|
|||||||
SIGNAL = None
|
SIGNAL = None
|
||||||
|
|
||||||
SYS_PLATFORM = None
|
SYS_PLATFORM = None
|
||||||
|
SYS_LANGUAGE = None
|
||||||
SYS_ENCODING = None
|
SYS_ENCODING = None
|
||||||
|
|
||||||
QUIET = False
|
QUIET = False
|
||||||
@@ -71,6 +74,7 @@ NOTIFY_QUEUE = Queue()
|
|||||||
INIT_LOCK = threading.Lock()
|
INIT_LOCK = threading.Lock()
|
||||||
_INITIALIZED = False
|
_INITIALIZED = False
|
||||||
_STARTED = False
|
_STARTED = False
|
||||||
|
_UPDATE = False
|
||||||
|
|
||||||
DATA_DIR = None
|
DATA_DIR = None
|
||||||
|
|
||||||
@@ -83,6 +87,8 @@ INSTALL_TYPE = None
|
|||||||
CURRENT_VERSION = None
|
CURRENT_VERSION = None
|
||||||
LATEST_VERSION = None
|
LATEST_VERSION = None
|
||||||
COMMITS_BEHIND = None
|
COMMITS_BEHIND = None
|
||||||
|
PREV_RELEASE = None
|
||||||
|
LATEST_RELEASE = None
|
||||||
|
|
||||||
UMASK = None
|
UMASK = None
|
||||||
|
|
||||||
@@ -90,8 +96,11 @@ HTTP_ROOT = None
|
|||||||
|
|
||||||
DEV = False
|
DEV = False
|
||||||
|
|
||||||
|
WEBSOCKET = None
|
||||||
WS_CONNECTED = False
|
WS_CONNECTED = False
|
||||||
PLEX_SERVER_UP = True
|
PLEX_SERVER_UP = None
|
||||||
|
|
||||||
|
TRACKER = None
|
||||||
|
|
||||||
|
|
||||||
def initialize(config_file):
|
def initialize(config_file):
|
||||||
@@ -102,7 +111,10 @@ def initialize(config_file):
|
|||||||
global _INITIALIZED
|
global _INITIALIZED
|
||||||
global CURRENT_VERSION
|
global CURRENT_VERSION
|
||||||
global LATEST_VERSION
|
global LATEST_VERSION
|
||||||
|
global PREV_RELEASE
|
||||||
global UMASK
|
global UMASK
|
||||||
|
global _UPDATE
|
||||||
|
|
||||||
CONFIG = plexpy.config.Config(config_file)
|
CONFIG = plexpy.config.Config(config_file)
|
||||||
CONFIG_FILE = config_file
|
CONFIG_FILE = config_file
|
||||||
|
|
||||||
@@ -175,17 +187,34 @@ def initialize(config_file):
|
|||||||
# Check if Tautulli has a uuid
|
# Check if Tautulli has a uuid
|
||||||
if CONFIG.PMS_UUID == '' or not CONFIG.PMS_UUID:
|
if CONFIG.PMS_UUID == '' or not CONFIG.PMS_UUID:
|
||||||
logger.debug(u"Generating UUID...")
|
logger.debug(u"Generating UUID...")
|
||||||
my_uuid = generate_uuid()
|
CONFIG.PMS_UUID = generate_uuid()
|
||||||
CONFIG.__setattr__('PMS_UUID', my_uuid)
|
|
||||||
CONFIG.write()
|
CONFIG.write()
|
||||||
|
|
||||||
# Check if Tautulli has an API key
|
# Check if Tautulli has an API key
|
||||||
if CONFIG.API_KEY == '':
|
if CONFIG.API_KEY == '':
|
||||||
logger.debug(u"Generating API key...")
|
logger.debug(u"Generating API key...")
|
||||||
api_key = generate_uuid()
|
CONFIG.API_KEY = generate_uuid()
|
||||||
CONFIG.__setattr__('API_KEY', api_key)
|
|
||||||
CONFIG.write()
|
CONFIG.write()
|
||||||
|
|
||||||
|
# Check if Tautulli has a jwt_secret
|
||||||
|
if CONFIG.JWT_SECRET == '' or not CONFIG.JWT_SECRET:
|
||||||
|
logger.debug(u"Generating JWT secret...")
|
||||||
|
CONFIG.JWT_SECRET = generate_uuid()
|
||||||
|
CONFIG.write()
|
||||||
|
|
||||||
|
# Get the previous version from the file
|
||||||
|
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
||||||
|
prev_version = None
|
||||||
|
if os.path.isfile(version_lock_file):
|
||||||
|
try:
|
||||||
|
with open(version_lock_file, "r") as fp:
|
||||||
|
prev_version = fp.read()
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"Unable to read previous version from file '%s': %s" %
|
||||||
|
(version_lock_file, e))
|
||||||
|
else:
|
||||||
|
prev_version = 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca'
|
||||||
|
|
||||||
# Get the currently installed version. Returns None, 'win32' or the git
|
# Get the currently installed version. Returns None, 'win32' or the git
|
||||||
# hash.
|
# hash.
|
||||||
CURRENT_VERSION, CONFIG.GIT_REMOTE, CONFIG.GIT_BRANCH = versioncheck.getVersion()
|
CURRENT_VERSION, CONFIG.GIT_REMOTE, CONFIG.GIT_BRANCH = versioncheck.getVersion()
|
||||||
@@ -194,8 +223,6 @@ def initialize(config_file):
|
|||||||
# This allowes one to restore to that version. The idea is that if we
|
# This allowes one to restore to that version. The idea is that if we
|
||||||
# arrive here, most parts of Tautulli seem to work.
|
# arrive here, most parts of Tautulli seem to work.
|
||||||
if CURRENT_VERSION:
|
if CURRENT_VERSION:
|
||||||
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(version_lock_file, "w") as fp:
|
with open(version_lock_file, "w") as fp:
|
||||||
fp.write(CURRENT_VERSION)
|
fp.write(CURRENT_VERSION)
|
||||||
@@ -213,6 +240,33 @@ def initialize(config_file):
|
|||||||
else:
|
else:
|
||||||
LATEST_VERSION = CURRENT_VERSION
|
LATEST_VERSION = CURRENT_VERSION
|
||||||
|
|
||||||
|
# Get the previous release from the file
|
||||||
|
release_file = os.path.join(DATA_DIR, "release.lock")
|
||||||
|
PREV_RELEASE = common.RELEASE
|
||||||
|
if os.path.isfile(release_file):
|
||||||
|
try:
|
||||||
|
with open(release_file, "r") as fp:
|
||||||
|
PREV_RELEASE = fp.read()
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"Unable to read previous release from file '%s': %s" %
|
||||||
|
(release_file, e))
|
||||||
|
elif prev_version == 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca': # Commit hash for v1.4.25
|
||||||
|
PREV_RELEASE = 'v1.4.25'
|
||||||
|
|
||||||
|
# Check if the release was updated
|
||||||
|
if common.RELEASE != PREV_RELEASE:
|
||||||
|
CONFIG.UPDATE_SHOW_CHANGELOG = 1
|
||||||
|
CONFIG.write()
|
||||||
|
_UPDATE = True
|
||||||
|
|
||||||
|
# Write current release version to file for update checking
|
||||||
|
try:
|
||||||
|
with open(release_file, "w") as fp:
|
||||||
|
fp.write(common.RELEASE)
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"Unable to write current release to file '%s': %s" %
|
||||||
|
(release_file, e))
|
||||||
|
|
||||||
# Get the real PMS urls for SSL and remote access
|
# Get the real PMS urls for SSL and remote access
|
||||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||||
plextv.get_server_resources()
|
plextv.get_server_resources()
|
||||||
@@ -232,6 +286,7 @@ def initialize(config_file):
|
|||||||
_INITIALIZED = True
|
_INITIALIZED = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def daemonize():
|
def daemonize():
|
||||||
if threading.activeCount() != 1:
|
if threading.activeCount() != 1:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -341,7 +396,7 @@ def initialize_scheduler():
|
|||||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=library_hours, minutes=0, seconds=0)
|
hours=library_hours, minutes=0, seconds=0)
|
||||||
|
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check server response',
|
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -359,12 +414,9 @@ def initialize_scheduler():
|
|||||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
# Schedule job to reconnect websocket
|
# Schedule job to reconnect server
|
||||||
response_seconds = CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS * CONFIG.WEBSOCKET_CONNECTION_TIMEOUT
|
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||||
response_seconds = 60 if response_seconds < 60 else response_seconds
|
hours=0, minutes=0, seconds=60, args=(False,))
|
||||||
|
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check server response',
|
|
||||||
hours=0, minutes=0, seconds=response_seconds)
|
|
||||||
|
|
||||||
# Start scheduler
|
# Start scheduler
|
||||||
if start_jobs and len(SCHED.get_jobs()):
|
if start_jobs and len(SCHED.get_jobs()):
|
||||||
@@ -406,6 +458,23 @@ def start():
|
|||||||
|
|
||||||
# Start background notification thread
|
# Start background notification thread
|
||||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||||
|
notifiers.check_browser_enabled()
|
||||||
|
|
||||||
|
if CONFIG.FIRST_RUN_COMPLETE:
|
||||||
|
activity_pinger.connect_server(log=True, startup=True)
|
||||||
|
|
||||||
|
if CONFIG.SYSTEM_ANALYTICS:
|
||||||
|
global TRACKER
|
||||||
|
TRACKER = initialize_tracker()
|
||||||
|
|
||||||
|
# Send system analytics events
|
||||||
|
if not CONFIG.FIRST_RUN_COMPLETE:
|
||||||
|
analytics_event(category='system', action='install')
|
||||||
|
|
||||||
|
elif _UPDATE:
|
||||||
|
analytics_event(category='system', action='update')
|
||||||
|
|
||||||
|
analytics_event(category='system', action='start')
|
||||||
|
|
||||||
_STARTED = True
|
_STARTED = True
|
||||||
|
|
||||||
@@ -498,7 +567,7 @@ def dbcheck():
|
|||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL, friendly_name TEXT, '
|
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL, friendly_name TEXT, '
|
||||||
'thumb TEXT, custom_avatar_url TEXT, email TEXT, is_home_user INTEGER DEFAULT NULL, '
|
'thumb TEXT, custom_avatar_url TEXT, email TEXT, is_admin INTEGER DEFAULT 0, is_home_user INTEGER DEFAULT NULL, '
|
||||||
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
|
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
|
||||||
'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0, allow_guest INTEGER DEFAULT 0, '
|
'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0, allow_guest INTEGER DEFAULT 0, '
|
||||||
'user_token TEXT, server_token TEXT, shared_libraries TEXT, filter_all TEXT, filter_movies TEXT, filter_tv TEXT, '
|
'user_token TEXT, server_token TEXT, shared_libraries TEXT, filter_all TEXT, filter_movies TEXT, filter_tv TEXT, '
|
||||||
@@ -554,7 +623,7 @@ def dbcheck():
|
|||||||
# poster_urls table :: This table keeps record of the notification poster urls
|
# poster_urls table :: This table keeps record of the notification poster urls
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS poster_urls (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS poster_urls (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
'rating_key INTEGER, poster_title TEXT, poster_url TEXT)'
|
'rating_key INTEGER, poster_title TEXT, poster_url TEXT, delete_hash TEXT)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# recently_added table :: This table keeps record of recently added items
|
# recently_added table :: This table keeps record of recently added items
|
||||||
@@ -1027,9 +1096,9 @@ def dbcheck():
|
|||||||
)
|
)
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'UPDATE session_history_media_info SET transcode_decision = (CASE '
|
'UPDATE session_history_media_info SET transcode_decision = (CASE '
|
||||||
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
||||||
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
||||||
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Upgrade session_history_media_info table from earlier versions
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
@@ -1188,6 +1257,26 @@ def dbcheck():
|
|||||||
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
|
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
|
try:
|
||||||
|
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
||||||
|
'WHERE stream_container IS NULL').fetchall()
|
||||||
|
if len(result) > 0:
|
||||||
|
logger.debug(u"Altering database. Removing NULL values from session_history_media_info table.")
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_container = "" WHERE stream_container IS NULL '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_video_codec = "" WHERE stream_video_codec IS NULL '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_audio_codec = "" WHERE stream_audio_codec IS NULL '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_subtitle_codec = "" WHERE stream_subtitle_codec IS NULL '
|
||||||
|
)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.warn(u"Unable to remove NULL values from session_history_media_info table.")
|
||||||
|
|
||||||
# Upgrade users table from earlier versions
|
# Upgrade users table from earlier versions
|
||||||
try:
|
try:
|
||||||
@@ -1264,6 +1353,15 @@ def dbcheck():
|
|||||||
'ALTER TABLE users ADD COLUMN filter_photos TEXT'
|
'ALTER TABLE users ADD COLUMN filter_photos TEXT'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade users table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT is_admin FROM users')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table users.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0'
|
||||||
|
)
|
||||||
|
|
||||||
# Upgrade notify_log table from earlier versions
|
# Upgrade notify_log table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT poster_url FROM notify_log')
|
c_db.execute('SELECT poster_url FROM notify_log')
|
||||||
@@ -1370,8 +1468,8 @@ def dbcheck():
|
|||||||
|
|
||||||
# Upgrade library_sections table from earlier versions (remove duplicated libraries)
|
# Upgrade library_sections table from earlier versions (remove duplicated libraries)
|
||||||
try:
|
try:
|
||||||
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""')
|
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""').fetchall()
|
||||||
if result.rowcount > 0:
|
if len(result) > 0:
|
||||||
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
|
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'DELETE FROM library_sections WHERE server_id = ""'
|
'DELETE FROM library_sections WHERE server_id = ""'
|
||||||
@@ -1489,32 +1587,51 @@ def dbcheck():
|
|||||||
'ALTER TABLE user_login ADD COLUMN success INTEGER DEFAULT 1'
|
'ALTER TABLE user_login ADD COLUMN success INTEGER DEFAULT 1'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade poster_urls table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT delete_hash FROM poster_urls')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table poster_urls.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE poster_urls ADD COLUMN delete_hash TEXT'
|
||||||
|
)
|
||||||
|
|
||||||
# Add "Local" user to database as default unauthenticated user.
|
# Add "Local" user to database as default unauthenticated user.
|
||||||
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
|
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
|
||||||
if not result.fetchone():
|
if not result.fetchone():
|
||||||
logger.debug(u"User 'Local' does not exist. Adding user.")
|
logger.debug(u"User 'Local' does not exist. Adding user.")
|
||||||
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
|
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
|
||||||
|
|
||||||
# Create table indices
|
# Create table indices
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup ON tvmaze_lookup (rating_key)'
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup ON tvmaze_lookup (rating_key)'
|
||||||
)
|
)
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_themoviedb_lookup ON themoviedb_lookup (rating_key)'
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_themoviedb_lookup ON themoviedb_lookup (rating_key)'
|
||||||
)
|
)
|
||||||
|
|
||||||
conn_db.commit()
|
conn_db.commit()
|
||||||
c_db.close()
|
c_db.close()
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
if CONFIG.UPDATE_NOTIFIERS_DB:
|
if CONFIG.UPDATE_NOTIFIERS_DB:
|
||||||
notifiers.upgrade_config_to_db()
|
notifiers.upgrade_config_to_db()
|
||||||
if CONFIG.UPDATE_LIBRARIES_DB_NOTIFY:
|
if CONFIG.UPDATE_LIBRARIES_DB_NOTIFY:
|
||||||
libraries.update_libraries_db_notify()
|
libraries.update_libraries_db_notify()
|
||||||
|
|
||||||
|
|
||||||
def shutdown(restart=False, update=False, checkout=False):
|
def shutdown(restart=False, update=False, checkout=False):
|
||||||
cherrypy.engine.exit()
|
cherrypy.engine.exit()
|
||||||
SCHED.shutdown(wait=False)
|
|
||||||
|
# Shutdown the websocket connection
|
||||||
|
if WEBSOCKET:
|
||||||
|
web_socket.shutdown()
|
||||||
|
|
||||||
|
if SCHED.running:
|
||||||
|
SCHED.shutdown(wait=False)
|
||||||
|
if activity_handler.ACTIVITY_SCHED.running:
|
||||||
|
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
|
||||||
|
|
||||||
# Stop the notification threads
|
# Stop the notification threads
|
||||||
for i in range(CONFIG.NOTIFICATION_THREADS):
|
for i in range(CONFIG.NOTIFICATION_THREADS):
|
||||||
@@ -1545,25 +1662,76 @@ def shutdown(restart=False, update=False, checkout=False):
|
|||||||
|
|
||||||
if restart:
|
if restart:
|
||||||
logger.info(u"Tautulli is restarting...")
|
logger.info(u"Tautulli is restarting...")
|
||||||
|
|
||||||
exe = sys.executable
|
exe = sys.executable
|
||||||
args = [exe, FULL_PATH]
|
args = [exe, FULL_PATH]
|
||||||
args += ARGS
|
args += ARGS
|
||||||
if '--nolaunch' not in args:
|
if '--nolaunch' not in args:
|
||||||
args += ['--nolaunch']
|
args += ['--nolaunch']
|
||||||
|
|
||||||
# os.execv fails with spaced names on Windows
|
# Separate out logger so we can shutdown logger after
|
||||||
# https://bugs.python.org/issue19066
|
|
||||||
if NOFORK:
|
if NOFORK:
|
||||||
logger.info('Running as service, not forking. Exiting...')
|
logger.info('Running as service, not forking. Exiting...')
|
||||||
elif os.name == 'nt':
|
elif os.name == 'nt':
|
||||||
logger.info('Restarting Tautulli with %s', args)
|
logger.info('Restarting Tautulli with %s', args)
|
||||||
subprocess.Popen(args, cwd=os.getcwd())
|
|
||||||
else:
|
else:
|
||||||
logger.info('Restarting Tautulli with %s', args)
|
logger.info('Restarting Tautulli with %s', args)
|
||||||
|
|
||||||
|
logger.shutdown()
|
||||||
|
|
||||||
|
# os.execv fails with spaced names on Windows
|
||||||
|
# https://bugs.python.org/issue19066
|
||||||
|
if NOFORK:
|
||||||
|
pass
|
||||||
|
elif os.name == 'nt':
|
||||||
|
subprocess.Popen(args, cwd=os.getcwd())
|
||||||
|
else:
|
||||||
os.execv(exe, args)
|
os.execv(exe, args)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.shutdown()
|
||||||
|
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
def generate_uuid():
|
def generate_uuid():
|
||||||
return uuid.uuid4().hex
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_tracker():
|
||||||
|
data = {
|
||||||
|
'dataSource': 'server',
|
||||||
|
'appName': 'Tautulli',
|
||||||
|
'appVersion': common.RELEASE,
|
||||||
|
'appId': plexpy.INSTALL_TYPE,
|
||||||
|
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
|
||||||
|
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_VERSION), # App Platform
|
||||||
|
'userLanguage': plexpy.SYS_LANGUAGE,
|
||||||
|
'documentEncoding': plexpy.SYS_ENCODING,
|
||||||
|
'noninteractive': True
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker = Tracker.create('UA-111522699-2', client_id=CONFIG.PMS_UUID, hash_client_id=True,
|
||||||
|
user_agent=common.USER_AGENT)
|
||||||
|
tracker.set(data)
|
||||||
|
|
||||||
|
return tracker
|
||||||
|
|
||||||
|
|
||||||
|
def analytics_event(category, action, label=None, value=None, **kwargs):
|
||||||
|
data = {'category': category, 'action': action}
|
||||||
|
|
||||||
|
if label is not None:
|
||||||
|
data['label'] = label
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
data['value'] = value
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
data.update(kwargs)
|
||||||
|
|
||||||
|
if TRACKER:
|
||||||
|
try:
|
||||||
|
TRACKER.send('event', data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Failed to send analytics event for category '%s', action '%s': %s" % (category, action, e))
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ ACTIVITY_SCHED = BackgroundScheduler()
|
|||||||
|
|
||||||
RECENTLY_ADDED_QUEUE = {}
|
RECENTLY_ADDED_QUEUE = {}
|
||||||
|
|
||||||
|
|
||||||
class ActivityHandler(object):
|
class ActivityHandler(object):
|
||||||
|
|
||||||
def __init__(self, timeline):
|
def __init__(self, timeline):
|
||||||
@@ -53,7 +54,7 @@ class ActivityHandler(object):
|
|||||||
|
|
||||||
def get_rating_key(self):
|
def get_rating_key(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
return int(self.timeline['ratingKey'])
|
return self.timeline['ratingKey']
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -64,14 +65,31 @@ class ActivityHandler(object):
|
|||||||
if session_list:
|
if session_list:
|
||||||
for session in session_list['sessions']:
|
for session in session_list['sessions']:
|
||||||
if int(session['session_key']) == self.get_session_key():
|
if int(session['session_key']) == self.get_session_key():
|
||||||
|
# Live sessions don't have rating keys in sessions
|
||||||
|
# Get it from the websocket data
|
||||||
|
if not session['rating_key']:
|
||||||
|
session['rating_key'] = self.get_rating_key()
|
||||||
return session
|
return session
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_db_session(self, session=None):
|
def update_db_session(self, session=None):
|
||||||
# Update our session temp table values
|
if session is None:
|
||||||
monitor_proc = activity_processor.ActivityProcessor()
|
session = self.get_live_session()
|
||||||
monitor_proc.write_session(session=session, notify=False)
|
|
||||||
|
if session:
|
||||||
|
# Update our session temp table values
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
ap.write_session(session=session, notify=False)
|
||||||
|
|
||||||
|
self.set_session_state()
|
||||||
|
|
||||||
|
def set_session_state(self):
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
ap.set_session_state(session_key=self.get_session_key(),
|
||||||
|
state=self.timeline['state'],
|
||||||
|
view_offset=self.timeline['viewOffset'],
|
||||||
|
stopped=int(time.time()))
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -92,14 +110,15 @@ class ActivityHandler(object):
|
|||||||
% (str(session['session_key']), str(session['user_id']), session['username'],
|
% (str(session['session_key']), str(session['user_id']), session['username'],
|
||||||
str(session['rating_key']), session['full_title']))
|
str(session['rating_key']), session['full_title']))
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': session, 'notify_action': 'on_play'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||||
|
|
||||||
# Write the new session to our temp session table
|
# Write the new session to our temp session table
|
||||||
self.update_db_session(session=session)
|
self.update_db_session(session=session)
|
||||||
|
|
||||||
def on_stop(self, force_stop=False):
|
def on_stop(self, force_stop=False):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
logger.debug(u"Tautulli ActivityHandler :: Session %s stopped." % str(self.get_session_key()))
|
logger.debug(u"Tautulli ActivityHandler :: Session %s %sstopped."
|
||||||
|
% (str(self.get_session_key()), 'force ' if force_stop else ''))
|
||||||
|
|
||||||
# Set the session last_paused timestamp
|
# Set the session last_paused timestamp
|
||||||
ap = activity_processor.ActivityProcessor()
|
ap = activity_processor.ActivityProcessor()
|
||||||
@@ -108,25 +127,28 @@ class ActivityHandler(object):
|
|||||||
# Update the session state and viewOffset
|
# Update the session state and viewOffset
|
||||||
# Set force_stop to true to disable the state set
|
# Set force_stop to true to disable the state set
|
||||||
if not force_stop:
|
if not force_stop:
|
||||||
ap.set_session_state(session_key=self.get_session_key(),
|
self.set_session_state()
|
||||||
state=self.timeline['state'],
|
|
||||||
view_offset=self.timeline['viewOffset'],
|
|
||||||
stopped=int(time.time()))
|
|
||||||
|
|
||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_stop'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
|
||||||
|
|
||||||
# Write it to the history table
|
# Write it to the history table
|
||||||
monitor_proc = activity_processor.ActivityProcessor()
|
monitor_proc = activity_processor.ActivityProcessor()
|
||||||
monitor_proc.write_session_history(session=db_session)
|
row_id = monitor_proc.write_session_history(session=db_session)
|
||||||
|
|
||||||
# Remove the session from our temp session table
|
if row_id:
|
||||||
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
|
||||||
% (str(self.get_session_key()), str(self.get_rating_key())))
|
|
||||||
ap.delete_session(session_key=self.get_session_key())
|
# Remove the session from our temp session table
|
||||||
delete_metadata_cache(self.get_session_key())
|
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
|
% (str(self.get_session_key()), str(self.get_rating_key())))
|
||||||
|
ap.delete_session(row_id=row_id)
|
||||||
|
delete_metadata_cache(self.get_session_key())
|
||||||
|
else:
|
||||||
|
schedule_callback('session_key-{}'.format(self.get_session_key()), func=force_stop_stream,
|
||||||
|
args=[self.get_session_key()], seconds=30)
|
||||||
|
|
||||||
def on_pause(self, still_paused=False):
|
def on_pause(self, still_paused=False):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -138,16 +160,13 @@ class ActivityHandler(object):
|
|||||||
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time()))
|
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time()))
|
||||||
|
|
||||||
# Update the session state and viewOffset
|
# Update the session state and viewOffset
|
||||||
ap.set_session_state(session_key=self.get_session_key(),
|
self.update_db_session()
|
||||||
state=self.timeline['state'],
|
|
||||||
view_offset=self.timeline['viewOffset'],
|
|
||||||
stopped=int(time.time()))
|
|
||||||
|
|
||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
if not still_paused:
|
if not still_paused:
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_pause'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
|
||||||
|
|
||||||
def on_resume(self):
|
def on_resume(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -158,15 +177,12 @@ class ActivityHandler(object):
|
|||||||
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None)
|
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None)
|
||||||
|
|
||||||
# Update the session state and viewOffset
|
# Update the session state and viewOffset
|
||||||
ap.set_session_state(session_key=self.get_session_key(),
|
self.update_db_session()
|
||||||
state=self.timeline['state'],
|
|
||||||
view_offset=self.timeline['viewOffset'],
|
|
||||||
stopped=int(time.time()))
|
|
||||||
|
|
||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_resume'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
|
||||||
|
|
||||||
def on_buffer(self):
|
def on_buffer(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -186,10 +202,7 @@ class ActivityHandler(object):
|
|||||||
buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key())
|
buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key())
|
||||||
|
|
||||||
# Update the session state and viewOffset
|
# Update the session state and viewOffset
|
||||||
ap.set_session_state(session_key=self.get_session_key(),
|
self.update_db_session()
|
||||||
state=self.timeline['state'],
|
|
||||||
view_offset=self.timeline['viewOffset'],
|
|
||||||
stopped=int(time.time()))
|
|
||||||
|
|
||||||
time_since_last_trigger = 0
|
time_since_last_trigger = 0
|
||||||
if buffer_last_triggered:
|
if buffer_last_triggered:
|
||||||
@@ -204,7 +217,7 @@ class ActivityHandler(object):
|
|||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_buffer'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
# This function receives events from our websocket connection
|
# This function receives events from our websocket connection
|
||||||
def process(self):
|
def process(self):
|
||||||
@@ -219,7 +232,7 @@ class ActivityHandler(object):
|
|||||||
if db_session:
|
if db_session:
|
||||||
# Re-schedule the callback to reset the 5 minutes timer
|
# Re-schedule the callback to reset the 5 minutes timer
|
||||||
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
||||||
function=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
||||||
|
|
||||||
last_state = db_session['state']
|
last_state = db_session['state']
|
||||||
last_key = str(db_session['rating_key'])
|
last_key = str(db_session['rating_key'])
|
||||||
@@ -229,9 +242,9 @@ class ActivityHandler(object):
|
|||||||
# Update the session state and viewOffset
|
# Update the session state and viewOffset
|
||||||
if this_state == 'playing':
|
if this_state == 'playing':
|
||||||
# Update the session in our temp session table
|
# Update the session in our temp session table
|
||||||
session = self.get_live_session()
|
# if the last set temporary stopped time exceeds 15 seconds
|
||||||
if session:
|
if int(time.time()) - db_session['stopped'] > 60:
|
||||||
self.update_db_session(session=session)
|
self.update_db_session()
|
||||||
|
|
||||||
# Start our state checks
|
# Start our state checks
|
||||||
if this_state != last_state:
|
if this_state != last_state:
|
||||||
@@ -242,9 +255,6 @@ class ActivityHandler(object):
|
|||||||
elif this_state == 'stopped':
|
elif this_state == 'stopped':
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
|
|
||||||
# Remove the callback if the stream is stopped
|
|
||||||
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
|
|
||||||
|
|
||||||
elif this_state == 'buffering':
|
elif this_state == 'buffering':
|
||||||
self.on_buffer()
|
self.on_buffer()
|
||||||
|
|
||||||
@@ -262,13 +272,25 @@ class ActivityHandler(object):
|
|||||||
# Monitor if the stream has reached the watch percentage for notifications
|
# Monitor if the stream has reached the watch percentage for notifications
|
||||||
# The only purpose of this is for notifications
|
# The only purpose of this is for notifications
|
||||||
if this_state != 'buffering':
|
if this_state != 'buffering':
|
||||||
progress_percent = helpers.get_percent(db_session['view_offset'], db_session['duration'])
|
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||||
notify_states = notification_handler.get_notify_state(session=db_session)
|
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||||
if (db_session['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
}
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_watched'})
|
|
||||||
|
if progress_percent >= watched_percent.get(db_session['media_type'], 101):
|
||||||
|
watched_notifiers = notification_handler.get_notify_state_enabled(
|
||||||
|
session=db_session, notify_action='on_watched', notified=False)
|
||||||
|
|
||||||
|
if watched_notifiers:
|
||||||
|
logger.debug(u"Tautulli ActivityHandler :: Session %s watched."
|
||||||
|
% str(self.get_session_key()))
|
||||||
|
|
||||||
|
for d in watched_notifiers:
|
||||||
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
||||||
|
'notifier_id': d['notifier_id'],
|
||||||
|
'notify_action': 'on_watched'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# We don't have this session in our table yet, start a new one.
|
# We don't have this session in our table yet, start a new one.
|
||||||
@@ -277,7 +299,7 @@ class ActivityHandler(object):
|
|||||||
|
|
||||||
# Schedule a callback to force stop a stale stream 5 minutes later
|
# Schedule a callback to force stop a stale stream 5 minutes later
|
||||||
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
||||||
function=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
||||||
|
|
||||||
|
|
||||||
class TimelineHandler(object):
|
class TimelineHandler(object):
|
||||||
@@ -321,6 +343,7 @@ class TimelineHandler(object):
|
|||||||
9: 'album',
|
9: 'album',
|
||||||
10: 'track'}
|
10: 'track'}
|
||||||
|
|
||||||
|
identifier = self.timeline.get('identifier')
|
||||||
state_type = self.timeline.get('state')
|
state_type = self.timeline.get('state')
|
||||||
media_type = media_types.get(self.timeline.get('type'))
|
media_type = media_types.get(self.timeline.get('type'))
|
||||||
section_id = self.timeline.get('sectionID', 0)
|
section_id = self.timeline.get('sectionID', 0)
|
||||||
@@ -329,6 +352,10 @@ class TimelineHandler(object):
|
|||||||
media_state = self.timeline.get('mediaState')
|
media_state = self.timeline.get('mediaState')
|
||||||
queue_size = self.timeline.get('queueSize')
|
queue_size = self.timeline.get('queueSize')
|
||||||
|
|
||||||
|
# Return if it is not a library event (i.e. DVR EPG event)
|
||||||
|
if identifier != 'com.plexapp.plugins.library':
|
||||||
|
return
|
||||||
|
|
||||||
# Add a new media item to the recently added queue
|
# Add a new media item to the recently added queue
|
||||||
if media_type and section_id > 0 and \
|
if media_type and section_id > 0 and \
|
||||||
((state_type == 0 and metadata_state == 'created')): # or \
|
((state_type == 0 and metadata_state == 'created')): # or \
|
||||||
@@ -355,7 +382,7 @@ class TimelineHandler(object):
|
|||||||
% (title, str(rating_key), str(grandparent_rating_key)))
|
% (title, str(rating_key), str(grandparent_rating_key)))
|
||||||
|
|
||||||
# Schedule a callback to clear the recently added queue
|
# Schedule a callback to clear the recently added queue
|
||||||
schedule_callback('rating_key-{}'.format(grandparent_rating_key), function=clear_recently_added_queue,
|
schedule_callback('rating_key-{}'.format(grandparent_rating_key), func=clear_recently_added_queue,
|
||||||
args=[grandparent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
args=[grandparent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
elif media_type in ('season', 'album'):
|
elif media_type in ('season', 'album'):
|
||||||
@@ -371,7 +398,7 @@ class TimelineHandler(object):
|
|||||||
% (title, str(rating_key), str(parent_rating_key)))
|
% (title, str(rating_key), str(parent_rating_key)))
|
||||||
|
|
||||||
# Schedule a callback to clear the recently added queue
|
# Schedule a callback to clear the recently added queue
|
||||||
schedule_callback('rating_key-{}'.format(parent_rating_key), function=clear_recently_added_queue,
|
schedule_callback('rating_key-{}'.format(parent_rating_key), func=clear_recently_added_queue,
|
||||||
args=[parent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
args=[parent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -382,7 +409,7 @@ class TimelineHandler(object):
|
|||||||
% (title, str(rating_key)))
|
% (title, str(rating_key)))
|
||||||
|
|
||||||
# Schedule a callback to clear the recently added queue
|
# Schedule a callback to clear the recently added queue
|
||||||
schedule_callback('rating_key-{}'.format(rating_key), function=clear_recently_added_queue,
|
schedule_callback('rating_key-{}'.format(rating_key), func=clear_recently_added_queue,
|
||||||
args=[rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
args=[rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
# A movie, show, or artist is done processing
|
# A movie, show, or artist is done processing
|
||||||
@@ -412,7 +439,7 @@ def del_keys(key):
|
|||||||
del_keys(RECENTLY_ADDED_QUEUE.pop(key))
|
del_keys(RECENTLY_ADDED_QUEUE.pop(key))
|
||||||
|
|
||||||
|
|
||||||
def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs):
|
def schedule_callback(id, func=None, remove_job=False, args=None, **kwargs):
|
||||||
if ACTIVITY_SCHED.get_job(id):
|
if ACTIVITY_SCHED.get_job(id):
|
||||||
if remove_job:
|
if remove_job:
|
||||||
ACTIVITY_SCHED.remove_job(id)
|
ACTIVITY_SCHED.remove_job(id)
|
||||||
@@ -422,7 +449,7 @@ def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs):
|
|||||||
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
||||||
elif not remove_job:
|
elif not remove_job:
|
||||||
ACTIVITY_SCHED.add_job(
|
ACTIVITY_SCHED.add_job(
|
||||||
function, args=args, id=id, trigger=DateTrigger(
|
func, args=args, id=id, trigger=DateTrigger(
|
||||||
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
||||||
|
|
||||||
|
|
||||||
@@ -430,13 +457,14 @@ def force_stop_stream(session_key):
|
|||||||
ap = activity_processor.ActivityProcessor()
|
ap = activity_processor.ActivityProcessor()
|
||||||
session = ap.get_session_by_key(session_key=session_key)
|
session = ap.get_session_by_key(session_key=session_key)
|
||||||
|
|
||||||
success = ap.write_session_history(session=session)
|
row_id = ap.write_session_history(session=session)
|
||||||
|
|
||||||
if success:
|
if row_id:
|
||||||
# If session is written to the databaase successfully, remove the session from the session table
|
# If session is written to the database successfully, remove the session from the session table
|
||||||
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||||
% (session['session_key'], session['rating_key']))
|
% (session['session_key'], session['rating_key']))
|
||||||
ap.delete_session(session_key=session_key)
|
ap.delete_session(row_id=row_id)
|
||||||
|
delete_metadata_cache(session_key)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
session['write_attempts'] += 1
|
session['write_attempts'] += 1
|
||||||
@@ -448,7 +476,7 @@ def force_stop_stream(session_key):
|
|||||||
ap.increment_write_attempts(session_key=session_key)
|
ap.increment_write_attempts(session_key=session_key)
|
||||||
|
|
||||||
# Reschedule for 30 seconds later
|
# Reschedule for 30 seconds later
|
||||||
schedule_callback('session_key={}'.format(session_key), function=force_stop_stream,
|
schedule_callback('session_key-{}'.format(session_key), func=force_stop_stream,
|
||||||
args=[session_key], seconds=30)
|
args=[session_key], seconds=30)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -495,16 +523,18 @@ def on_created(rating_key, **kwargs):
|
|||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
notify = True
|
notify = True
|
||||||
now = int(time.time())
|
# now = int(time.time())
|
||||||
|
#
|
||||||
if helpers.cast_to_int(metadata['updated_at']) < now - 86400: # Updated more than 24 hours ago
|
# if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
|
||||||
logger.debug(u"Tautulli TimelineHandler :: Library item %s updated more than 24 hours ago. Not notifying." % str(rating_key))
|
# logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
|
||||||
notify = False
|
# % str(rating_key))
|
||||||
|
# notify = False
|
||||||
|
|
||||||
data_factory = datafactory.DataFactory()
|
data_factory = datafactory.DataFactory()
|
||||||
if 'child_keys' not in kwargs:
|
if 'child_keys' not in kwargs:
|
||||||
if data_factory.get_recently_added_item(rating_key):
|
if data_factory.get_recently_added_item(rating_key):
|
||||||
logger.debug(u"Tautulli TimelineHandler :: Library item %s added already. Not notifying again." % str(rating_key))
|
logger.debug(u"Tautulli TimelineHandler :: Library item %s added already. Not notifying again."
|
||||||
|
% str(rating_key))
|
||||||
notify = False
|
notify = False
|
||||||
|
|
||||||
if notify:
|
if notify:
|
||||||
@@ -527,7 +557,7 @@ def on_created(rating_key, **kwargs):
|
|||||||
|
|
||||||
def delete_metadata_cache(session_key):
|
def delete_metadata_cache(session_key):
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % session_key))
|
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % session_key))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
|
logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
|
||||||
% (session_key, e))
|
% (session_key, e))
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ def check_active_sessions(ws_request=False):
|
|||||||
if session['state'] == 'paused':
|
if session['state'] == 'paused':
|
||||||
logger.debug(u"Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
logger.debug(u"Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_pause'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
|
||||||
|
|
||||||
if session['state'] == 'playing' and stream['state'] == 'paused':
|
if session['state'] == 'playing' and stream['state'] == 'paused':
|
||||||
logger.debug(u"Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
logger.debug(u"Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_resume'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
|
||||||
|
|
||||||
if stream['state'] == 'paused' and not ws_request:
|
if stream['state'] == 'paused' and not ws_request:
|
||||||
# The stream is still paused so we need to increment the paused_counter
|
# The stream is still paused so we need to increment the paused_counter
|
||||||
@@ -104,7 +104,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_buffer'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Subsequent buffer notifications after wait time
|
# Subsequent buffer notifications after wait time
|
||||||
@@ -118,7 +118,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_buffer'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
logger.debug(u"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
logger.debug(u"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||||
% (stream['session_key'],
|
% (stream['session_key'],
|
||||||
@@ -135,7 +135,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_watched'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# The user has stopped playing a stream
|
# The user has stopped playing a stream
|
||||||
@@ -155,19 +155,18 @@ def check_active_sessions(ws_request=False):
|
|||||||
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_watched'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_stop'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
|
||||||
|
|
||||||
# Write the item history on playback stop
|
# Write the item history on playback stop
|
||||||
success = monitor_process.write_session_history(session=stream)
|
row_id = monitor_process.write_session_history(session=stream)
|
||||||
|
|
||||||
if success:
|
if row_id:
|
||||||
# If session is written to the databaase successfully, remove the session from the session table
|
# If session is written to the databaase successfully, remove the session from the session table
|
||||||
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
% (stream['session_key'], stream['rating_key']))
|
% (stream['session_key'], stream['rating_key']))
|
||||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
monitor_process.delete_session(row_id=row_id)
|
||||||
[stream['session_key'], stream['rating_key']])
|
|
||||||
else:
|
else:
|
||||||
stream['write_attempts'] += 1
|
stream['write_attempts'] += 1
|
||||||
|
|
||||||
@@ -175,18 +174,14 @@ def check_active_sessions(ws_request=False):
|
|||||||
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||||
"Will try again on the next pass. Write attempt %s."
|
"Will try again on the next pass. Write attempt %s."
|
||||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||||
monitor_db.action('UPDATE sessions SET write_attempts = ? '
|
monitor_process.increment_write_attempts(session_key=stream['session_key'])
|
||||||
'WHERE session_key = ? AND rating_key = ?',
|
|
||||||
[stream['write_attempts'], stream['session_key'], stream['rating_key']])
|
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||||
"Removing session from the database. Write attempt %s."
|
"Removing session from the database. Write attempt %s."
|
||||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||||
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
% (stream['session_key'], stream['rating_key']))
|
% (stream['session_key'], stream['rating_key']))
|
||||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
monitor_process.delete_session(session_key=stream['session_key'])
|
||||||
[stream['session_key'], stream['rating_key']])
|
|
||||||
|
|
||||||
|
|
||||||
# Process the newly received session data
|
# Process the newly received session data
|
||||||
for session in media_container:
|
for session in media_container:
|
||||||
@@ -248,7 +243,7 @@ def check_recently_added():
|
|||||||
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||||
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'})
|
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
item = max(metadata, key=lambda x:x['added_at'])
|
item = max(metadata, key=lambda x:x['added_at'])
|
||||||
@@ -266,15 +261,40 @@ def check_recently_added():
|
|||||||
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
||||||
|
|
||||||
# Check if any notification agents have notifications enabled
|
# Check if any notification agents have notifications enabled
|
||||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'})
|
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||||
|
|
||||||
|
|
||||||
def check_server_response():
|
def connect_server(log=True, startup=False):
|
||||||
logger.info(u"Tautulli Monitor :: Attempting to reconnect Plex server...")
|
if plexpy.CONFIG.PMS_IS_CLOUD:
|
||||||
try:
|
if log:
|
||||||
web_socket.start_thread()
|
logger.info(u"Tautulli Monitor :: Checking for Plex Cloud server status...")
|
||||||
except:
|
|
||||||
logger.warn(u"Websocket :: Unable to open connection.")
|
plex_tv = plextv.PlexTV()
|
||||||
|
status = plex_tv.get_cloud_server_status()
|
||||||
|
|
||||||
|
if status is True:
|
||||||
|
logger.info(u"Tautulli Monitor :: Plex Cloud server is active.")
|
||||||
|
elif status is False:
|
||||||
|
if log:
|
||||||
|
logger.info(u"Tautulli Monitor :: Plex Cloud server is sleeping.")
|
||||||
|
else:
|
||||||
|
if log:
|
||||||
|
logger.error(u"Tautulli Monitor :: Failed to retrieve Plex Cloud server status.")
|
||||||
|
|
||||||
|
if not status and startup:
|
||||||
|
web_socket.on_disconnect()
|
||||||
|
|
||||||
|
else:
|
||||||
|
status = True
|
||||||
|
|
||||||
|
if status:
|
||||||
|
if log and not startup:
|
||||||
|
logger.info(u"Tautulli Monitor :: Attempting to reconnect Plex server...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
web_socket.start_thread()
|
||||||
|
except:
|
||||||
|
logger.error(u"Websocket :: Unable to open connection.")
|
||||||
|
|
||||||
|
|
||||||
def check_server_access():
|
def check_server_access():
|
||||||
@@ -330,4 +350,4 @@ def check_server_updates():
|
|||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info(u"Tautulli Monitor :: No PMS update available.")
|
logger.info(u"Tautulli Monitor :: No PMS update available.")
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class ActivityProcessor(object):
|
|||||||
if result == 'insert':
|
if result == 'insert':
|
||||||
# Check if any notification agents have notifications enabled
|
# Check if any notification agents have notifications enabled
|
||||||
if notify:
|
if notify:
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': values, 'notify_action': 'on_play'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': values.copy(), 'notify_action': 'on_play'})
|
||||||
|
|
||||||
# If it's our first write then time stamp it.
|
# If it's our first write then time stamp it.
|
||||||
started = int(time.time())
|
started = int(time.time())
|
||||||
@@ -155,7 +155,12 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
# Reload json from raw stream info
|
# Reload json from raw stream info
|
||||||
if session.get('raw_stream_info'):
|
if session.get('raw_stream_info'):
|
||||||
session.update(json.loads(session['raw_stream_info']))
|
raw_stream_info = json.loads(session['raw_stream_info'])
|
||||||
|
# Don't overwrite id, session_key, stopped
|
||||||
|
raw_stream_info.pop('id', None)
|
||||||
|
raw_stream_info.pop('session_key', None)
|
||||||
|
raw_stream_info.pop('stopped', None)
|
||||||
|
session.update(raw_stream_info)
|
||||||
|
|
||||||
session = defaultdict(str, session)
|
session = defaultdict(str, session)
|
||||||
|
|
||||||
@@ -177,6 +182,7 @@ class ActivityProcessor(object):
|
|||||||
else:
|
else:
|
||||||
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
||||||
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
||||||
|
return session['id']
|
||||||
|
|
||||||
if str(session['paused_counter']).isdigit():
|
if str(session['paused_counter']).isdigit():
|
||||||
real_play_time = stopped - session['started'] - int(session['paused_counter'])
|
real_play_time = stopped - session['started'] - int(session['paused_counter'])
|
||||||
@@ -229,7 +235,8 @@ class ActivityProcessor(object):
|
|||||||
## TODO: Fix media info from imports. Temporary media info from import session.
|
## TODO: Fix media info from imports. Temporary media info from import session.
|
||||||
media_info = session
|
media_info = session
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history table...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write sessionKey %s to session_history table..."
|
||||||
|
# % session['session_key'])
|
||||||
keys = {'id': None}
|
keys = {'id': None}
|
||||||
values = {'started': session['started'],
|
values = {'started': session['started'],
|
||||||
'stopped': stopped,
|
'stopped': stopped,
|
||||||
@@ -254,7 +261,8 @@ class ActivityProcessor(object):
|
|||||||
'view_offset': session['view_offset']
|
'view_offset': session['view_offset']
|
||||||
}
|
}
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history transaction...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history transaction..."
|
||||||
|
# % session['session_key'])
|
||||||
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Check if we should group the session, select the last two rows from the user
|
# Check if we should group the session, select the last two rows from the user
|
||||||
@@ -284,7 +292,7 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
||||||
if prev_session == new_session == None:
|
if prev_session is None and new_session is None:
|
||||||
args = [last_id, last_id]
|
args = [last_id, last_id]
|
||||||
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
||||||
args = [prev_session['reference_id'], new_session['id']]
|
args = [prev_session['reference_id'], new_session['id']]
|
||||||
@@ -298,7 +306,8 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
# Write the session_history_media_info table
|
# Write the session_history_media_info table
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history_media_info table...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_media_info table..."
|
||||||
|
# % session['session_key'])
|
||||||
keys = {'id': last_id}
|
keys = {'id': last_id}
|
||||||
values = {'rating_key': session['rating_key'],
|
values = {'rating_key': session['rating_key'],
|
||||||
'video_decision': session['video_decision'],
|
'video_decision': session['video_decision'],
|
||||||
@@ -365,7 +374,8 @@ class ActivityProcessor(object):
|
|||||||
'optimized_version_title': session['optimized_version_title']
|
'optimized_version_title': session['optimized_version_title']
|
||||||
}
|
}
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_media_info transaction...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_media_info transaction..."
|
||||||
|
# % session['session_key'])
|
||||||
self.db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Write the session_history_metadata table
|
# Write the session_history_metadata table
|
||||||
@@ -375,7 +385,8 @@ class ActivityProcessor(object):
|
|||||||
genres = ";".join(metadata['genres'])
|
genres = ";".join(metadata['genres'])
|
||||||
labels = ";".join(metadata['labels'])
|
labels = ";".join(metadata['labels'])
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history_metadata table...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_metadata table..."
|
||||||
|
# % session['session_key'])
|
||||||
keys = {'id': last_id}
|
keys = {'id': last_id}
|
||||||
values = {'rating_key': session['rating_key'],
|
values = {'rating_key': session['rating_key'],
|
||||||
'parent_rating_key': session['parent_rating_key'],
|
'parent_rating_key': session['parent_rating_key'],
|
||||||
@@ -411,11 +422,12 @@ class ActivityProcessor(object):
|
|||||||
'labels': labels
|
'labels': labels
|
||||||
}
|
}
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_metadata transaction...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_metadata transaction..."
|
||||||
|
# % session['session_key'])
|
||||||
self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Return true when the session is successfully written to the database
|
# Return the session row id when the session is successfully written to the database
|
||||||
return True
|
return session['id']
|
||||||
|
|
||||||
def get_sessions(self, user_id=None, ip_address=None):
|
def get_sessions(self, user_id=None, ip_address=None):
|
||||||
query = 'SELECT * FROM sessions'
|
query = 'SELECT * FROM sessions'
|
||||||
@@ -456,9 +468,11 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_session(self, session_key=None):
|
def delete_session(self, session_key=None, row_id=None):
|
||||||
if str(session_key).isdigit():
|
if str(session_key).isdigit():
|
||||||
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
||||||
|
elif str(row_id).isdigit():
|
||||||
|
self.db.action('DELETE FROM sessions WHERE id = ?', [row_id])
|
||||||
|
|
||||||
def set_session_last_paused(self, session_key=None, timestamp=None):
|
def set_session_last_paused(self, session_key=None, timestamp=None):
|
||||||
if str(session_key).isdigit():
|
if str(session_key).isdigit():
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import database
|
|||||||
import libraries
|
import libraries
|
||||||
import logger
|
import logger
|
||||||
import mobile_app
|
import mobile_app
|
||||||
|
import notification_handler
|
||||||
|
import notifiers
|
||||||
import users
|
import users
|
||||||
|
|
||||||
|
|
||||||
@@ -165,8 +167,8 @@ class API2:
|
|||||||
"""
|
"""
|
||||||
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)
|
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)
|
||||||
templog = []
|
templog = []
|
||||||
start = int(kwargs.get('start', 0))
|
start = int(start)
|
||||||
end = int(kwargs.get('end', 0))
|
end = int(end)
|
||||||
|
|
||||||
if regex:
|
if regex:
|
||||||
logger.api_debug(u'Tautulli APIv2 :: Filtering log using regex %s' % regex)
|
logger.api_debug(u'Tautulli APIv2 :: Filtering log using regex %s' % regex)
|
||||||
@@ -333,14 +335,14 @@ class API2:
|
|||||||
""" Restart Tautulli."""
|
""" Restart Tautulli."""
|
||||||
|
|
||||||
plexpy.SIGNAL = 'restart'
|
plexpy.SIGNAL = 'restart'
|
||||||
self._api_msg = 'Restarting plexpy'
|
self._api_msg = 'Restarting Tautulli'
|
||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
""" Check for Tautulli updates on Github."""
|
""" Update Tautulli."""
|
||||||
|
|
||||||
plexpy.SIGNAL = 'update'
|
plexpy.SIGNAL = 'update'
|
||||||
self._api_msg = 'Updating plexpy'
|
self._api_msg = 'Updating Tautulli'
|
||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
|
|
||||||
def refresh_libraries_list(self, **kwargs):
|
def refresh_libraries_list(self, **kwargs):
|
||||||
@@ -397,6 +399,50 @@ class API2:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def notify(self, notifier_id='', subject='Tautulli', body='Test notification', **kwargs):
|
||||||
|
""" Send a notification using Tautulli.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
notifier_id (int): The ID number of the notification agent
|
||||||
|
subject (str): The subject of the message
|
||||||
|
body (str): The body of the message
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if not notifier_id:
|
||||||
|
self._api_msg = 'Notification failed: no notifier id provided.'
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
return
|
||||||
|
|
||||||
|
notifier = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||||
|
|
||||||
|
if not notifier:
|
||||||
|
self._api_msg = 'Notification failed: invalid notifier_id provided %s.' % notifier_id
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.api_debug(u'Tautulli APIv2 :: Sending notification.')
|
||||||
|
success = notification_handler.notify(notifier_id=notifier_id,
|
||||||
|
notify_action='api',
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self._api_msg = 'Notification sent.'
|
||||||
|
self._api_result_type = 'success'
|
||||||
|
else:
|
||||||
|
self._api_msg = 'Notification failed.'
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def _api_make_md(self):
|
def _api_make_md(self):
|
||||||
""" Tries to make a API.md to simplify the api docs. """
|
""" Tries to make a API.md to simplify the api docs. """
|
||||||
|
|
||||||
@@ -581,8 +627,8 @@ General optional parameters:
|
|||||||
if isinstance(result, (dict, list)):
|
if isinstance(result, (dict, list)):
|
||||||
ret = result
|
ret = result
|
||||||
else:
|
else:
|
||||||
raise
|
raise Exception
|
||||||
except:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
ret = json.loads(result)
|
ret = json.loads(result)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@@ -606,10 +652,9 @@ General optional parameters:
|
|||||||
# {result: error, message: 'Some shit happend'}
|
# {result: error, message: 'Some shit happend'}
|
||||||
if isinstance(ret, dict):
|
if isinstance(ret, dict):
|
||||||
if ret.get('message'):
|
if ret.get('message'):
|
||||||
self._api_msg = ret.get('message', {})
|
self._api_msg = ret.pop('message', None)
|
||||||
ret = {}
|
|
||||||
|
|
||||||
if ret.get('result'):
|
if ret.get('result'):
|
||||||
self._api_result_type = ret.get('result')
|
self._api_result_type = ret.pop('result', None)
|
||||||
|
|
||||||
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ from collections import OrderedDict
|
|||||||
import version
|
import version
|
||||||
|
|
||||||
# Identify Our Application
|
# Identify Our Application
|
||||||
USER_AGENT = 'Tautulli/-' + version.PLEXPY_BRANCH + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
|
|
||||||
' ' + platform.release() + ')'
|
|
||||||
|
|
||||||
PLATFORM = platform.system()
|
PLATFORM = platform.system()
|
||||||
PLATFORM_VERSION = platform.release()
|
PLATFORM_VERSION = platform.release()
|
||||||
BRANCH = version.PLEXPY_BRANCH
|
BRANCH = version.PLEXPY_BRANCH
|
||||||
VERSION_NUMBER = version.PLEXPY_RELEASE_VERSION
|
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||||
|
|
||||||
|
USER_AGENT = 'Tautulli/{} ({} {})'.format(RELEASE, PLATFORM, PLATFORM_VERSION)
|
||||||
|
|
||||||
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
||||||
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||||
@@ -174,11 +173,11 @@ HW_ENCODERS = [
|
|||||||
|
|
||||||
SCHEDULER_LIST = [
|
SCHEDULER_LIST = [
|
||||||
'Check GitHub for updates',
|
'Check GitHub for updates',
|
||||||
|
'Check for server response',
|
||||||
'Check for active sessions',
|
'Check for active sessions',
|
||||||
'Check for recently added items',
|
'Check for recently added items',
|
||||||
'Check for Plex updates',
|
'Check for Plex updates',
|
||||||
'Check for Plex remote access',
|
'Check for Plex remote access',
|
||||||
'Check server response',
|
|
||||||
'Refresh users list',
|
'Refresh users list',
|
||||||
'Refresh libraries list',
|
'Refresh libraries list',
|
||||||
'Refresh Plex server URLs',
|
'Refresh Plex server URLs',
|
||||||
@@ -279,16 +278,22 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{
|
{
|
||||||
'category': 'Global',
|
'category': 'Global',
|
||||||
'parameters': [
|
'parameters': [
|
||||||
{'name': 'Tautulli Version', 'type': 'str', 'value': 'plexpy_version', 'description': 'The current version of Tautulli.'},
|
{'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', 'description': 'The current version of Tautulli.'},
|
||||||
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'plexpy_branch', 'description': 'The current git branch of Tautulli.'},
|
{'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', 'description': 'The current git remote of Tautulli.'},
|
||||||
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'plexpy_commit', 'description': 'The current git commit hash of Tautulli.'},
|
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', 'description': 'The current git branch of Tautulli.'},
|
||||||
|
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', 'description': 'The current git commit hash of Tautulli.'},
|
||||||
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
|
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
|
||||||
{'name': 'Server Uptime', 'type': 'str', 'value': 'server_uptime', 'description': 'The uptime (in days, hours, mins, secs) of your Plex Server.'},
|
{'name': 'Server IP', 'type': 'str', 'value': 'server_ip', 'description': 'The connection IP address for your Plex Server.'},
|
||||||
|
{'name': 'Server Port', 'type': 'int', 'value': 'server_port', 'description': 'The connection port for your Plex Server.'},
|
||||||
|
{'name': 'Server URL', 'type': 'str', 'value': 'server_url', 'description': 'The connection URL for your Plex Server.'},
|
||||||
|
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', 'description': 'The platform of your Plex Server.'},
|
||||||
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
|
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
|
||||||
|
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
|
||||||
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
|
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
|
||||||
{'name': 'Datestamp', 'type': 'int', 'value': 'datestamp', 'description': 'The date (in date format) the notification was triggered.'},
|
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification was triggered.'},
|
||||||
{'name': 'Timestamp', 'type': 'int', 'value': 'timestamp', 'description': 'The time (in time format) the notification was triggered.'},
|
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification was triggered.'},
|
||||||
]
|
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification was triggered.'},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'category': 'Stream Details',
|
'category': 'Stream Details',
|
||||||
@@ -386,22 +391,28 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
|
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
|
||||||
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'},
|
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'},
|
||||||
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
|
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
|
||||||
|
{'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'},
|
||||||
|
{'name': 'Episode Count', 'type': 'int', 'value': 'episode_count', 'description': 'The number of episodes.'},
|
||||||
|
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
|
||||||
|
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
|
||||||
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
|
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
|
||||||
{'name': 'Release Date', 'type': 'int', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'},
|
{'name': 'Release Date', 'type': 'int', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'},
|
||||||
{'name': 'Air Date', 'type': 'int', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'},
|
{'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'},
|
||||||
{'name': 'Added Date', 'type': 'int', 'value': 'added_date', 'description': 'The date (in date format) the item was added to Plex.'},
|
{'name': 'Added Date', 'type': 'str', 'value': 'added_date', 'description': 'The date (in date format) the item was added to Plex.'},
|
||||||
{'name': 'Updated Date', 'type': 'int', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'},
|
{'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'},
|
||||||
{'name': 'Last Viewed Date', 'type': 'int', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
|
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
|
||||||
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
|
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
|
||||||
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
||||||
{'name': 'Director', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
|
{'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
|
||||||
{'name': 'Writer', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
|
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
|
||||||
{'name': 'Actor', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
|
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
|
||||||
{'name': 'Genre', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
|
{'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
|
||||||
|
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
|
||||||
|
{'name': 'Collections', 'type': 'str', 'value': 'collections', 'description': 'A list of collections for the item.'},
|
||||||
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
|
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
|
||||||
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
|
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
|
||||||
{'name': 'Rating', 'type': 'int', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
|
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
|
||||||
{'name': 'Audience Rating', 'type': 'int', 'value': 'audience_rating', 'description': 'The audience rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
|
{'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating', 'description': 'The audience rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
|
||||||
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
|
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
|
||||||
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
|
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
|
||||||
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'},
|
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'},
|
||||||
@@ -461,28 +472,29 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{
|
{
|
||||||
'category': 'Plex Update Available',
|
'category': 'Plex Update Available',
|
||||||
'parameters': [
|
'parameters': [
|
||||||
{'name': 'Update Version', 'type': 'int', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'},
|
{'name': 'Update Version', 'type': 'str', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'},
|
||||||
{'name': 'Update Url', 'type': 'int', 'value': 'update_url', 'description': 'The download URL for the available update.'},
|
{'name': 'Update Url', 'type': 'str', 'value': 'update_url', 'description': 'The download URL for the available update.'},
|
||||||
{'name': 'Update Release Date', 'type': 'int', 'value': 'update_release_date', 'description': 'The release date of the available update.'},
|
{'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', 'description': 'The release date of the available update.'},
|
||||||
{'name': 'Update Channel', 'type': 'int', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'},
|
{'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'},
|
||||||
{'name': 'Update Platform', 'type': 'int', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'},
|
{'name': 'Update Platform', 'type': 'str', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'},
|
||||||
{'name': 'Update Distro', 'type': 'int', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'},
|
{'name': 'Update Distro', 'type': 'str', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'},
|
||||||
{'name': 'Update Distro Build', 'type': 'int', 'value': 'update_distro_build', 'description': 'The distro build of your Plex Server.'},
|
{'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build', 'description': 'The distro build of your Plex Server.'},
|
||||||
{'name': 'Update Requirements', 'type': 'int', 'value': 'update_requirements', 'description': 'The requirements for the available update.'},
|
{'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements', 'description': 'The requirements for the available update.'},
|
||||||
{'name': 'Update Extra Info', 'type': 'int', 'value': 'update_extra_info', 'description': 'Any extra info for the available update.'},
|
{'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info', 'description': 'Any extra info for the available update.'},
|
||||||
{'name': 'Update Changelog Added', 'type': 'int', 'value': 'update_changelog_added', 'description': 'The added changelog for the available update.'},
|
{'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added', 'description': 'The added changelog for the available update.'},
|
||||||
{'name': 'Update Changelog Fixed', 'type': 'int', 'value': 'update_changelog_fixed', 'description': 'The fixed changelog for the available update.'},
|
{'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed', 'description': 'The fixed changelog for the available update.'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'category': 'Tautulli Update Available',
|
'category': 'Tautulli Update Available',
|
||||||
'parameters': [
|
'parameters': [
|
||||||
{'name': 'Plexpy Update Version', 'type': 'int', 'value': 'plexpy_update_version', 'description': 'The available update version for Tautulli.'},
|
{'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'},
|
||||||
{'name': 'Plexpy Update Tar', 'type': 'int', 'value': 'plexpy_update_tar', 'description': 'The tar download URL for the available update.'},
|
{'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url', 'description': 'The release page URL on GitHub'},
|
||||||
{'name': 'Plexpy Update Zip', 'type': 'int', 'value': 'plexpy_update_zip', 'description': 'The zip download URL for the available update.'},
|
{'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'},
|
||||||
{'name': 'Plexpy Update Commit', 'type': 'int', 'value': 'plexpy_update_commit', 'description': 'The commit hash for the available update.'},
|
{'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'},
|
||||||
{'name': 'Plexpy Update Behind', 'type': 'int', 'value': 'plexpy_update_behind', 'description': 'The number of commits behind for the available update.'},
|
{'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'},
|
||||||
{'name': 'Plexpy Update Changelog', 'type': 'int', 'value': 'plexpy_update_changelog', 'description': 'The changelog for the available update.'},
|
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'},
|
||||||
|
{'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PMS_PLEXPASS': (int, 'PMS', 0),
|
'PMS_PLEXPASS': (int, 'PMS', 0),
|
||||||
'PMS_PLATFORM': (str, 'PMS', ''),
|
'PMS_PLATFORM': (str, 'PMS', ''),
|
||||||
'PMS_VERSION': (str, 'PMS', ''),
|
'PMS_VERSION': (str, 'PMS', ''),
|
||||||
'PMS_UPDATE_CHANNEL': (str, 'PMS', 'public'),
|
'PMS_UPDATE_CHANNEL': (str, 'PMS', 'plex'),
|
||||||
'PMS_UPDATE_DISTRO': (str, 'PMS', ''),
|
'PMS_UPDATE_DISTRO': (str, 'PMS', ''),
|
||||||
'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''),
|
'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''),
|
||||||
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
||||||
@@ -176,13 +176,13 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'GIT_PATH': (str, 'General', ''),
|
'GIT_PATH': (str, 'General', ''),
|
||||||
'GIT_REMOTE': (str, 'General', 'origin'),
|
'GIT_REMOTE': (str, 'General', 'origin'),
|
||||||
'GIT_TOKEN': (str, 'General', ''),
|
'GIT_TOKEN': (str, 'General', ''),
|
||||||
'GIT_USER': (str, 'General', 'JonnyWong16'),
|
'GIT_USER': (str, 'General', 'Tautulli'),
|
||||||
'GIT_REPO': (str, 'General', 'plexpy'),
|
'GIT_REPO': (str, 'General', 'Tautulli'),
|
||||||
'GRAPH_TYPE': (str, 'General', 'plays'),
|
'GRAPH_TYPE': (str, 'General', 'plays'),
|
||||||
'GRAPH_DAYS': (int, 'General', 30),
|
'GRAPH_DAYS': (int, 'General', 30),
|
||||||
'GRAPH_MONTHS': (int, 'General', 12),
|
'GRAPH_MONTHS': (int, 'General', 12),
|
||||||
'GRAPH_TAB': (str, 'General', 'tabs-1'),
|
'GRAPH_TAB': (str, 'General', 'tabs-1'),
|
||||||
'GROUP_HISTORY_TABLES': (int, 'General', 0),
|
'GROUP_HISTORY_TABLES': (int, 'General', 1),
|
||||||
'GROWL_ENABLED': (int, 'Growl', 0),
|
'GROWL_ENABLED': (int, 'Growl', 0),
|
||||||
'GROWL_HOST': (str, 'Growl', ''),
|
'GROWL_HOST': (str, 'Growl', ''),
|
||||||
'GROWL_PASSWORD': (str, 'Growl', ''),
|
'GROWL_PASSWORD': (str, 'Growl', ''),
|
||||||
@@ -225,6 +225,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'HTTP_PROXY': (int, 'General', 0),
|
'HTTP_PROXY': (int, 'General', 0),
|
||||||
'HTTP_ROOT': (str, 'General', ''),
|
'HTTP_ROOT': (str, 'General', ''),
|
||||||
'HTTP_USERNAME': (str, 'General', ''),
|
'HTTP_USERNAME': (str, 'General', ''),
|
||||||
|
'HTTP_PLEX_ADMIN': (int, 'General', 0),
|
||||||
'HIPCHAT_URL': (str, 'Hipchat', ''),
|
'HIPCHAT_URL': (str, 'Hipchat', ''),
|
||||||
'HIPCHAT_COLOR': (str, 'Hipchat', ''),
|
'HIPCHAT_COLOR': (str, 'Hipchat', ''),
|
||||||
'HIPCHAT_INCL_SUBJECT': (int, 'Hipchat', 1),
|
'HIPCHAT_INCL_SUBJECT': (int, 'Hipchat', 1),
|
||||||
@@ -289,6 +290,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'LOG_BLACKLIST': (int, 'General', 1),
|
'LOG_BLACKLIST': (int, 'General', 1),
|
||||||
'LOG_DIR': (str, 'General', ''),
|
'LOG_DIR': (str, 'General', ''),
|
||||||
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
||||||
|
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
|
||||||
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||||
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||||
@@ -478,6 +480,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
||||||
'REMOTE_ACCESS_PING_THRESHOLD': (int, 'Advanced', 3),
|
'REMOTE_ACCESS_PING_THRESHOLD': (int, 'Advanced', 3),
|
||||||
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
|
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
|
||||||
|
'SHOW_ADVANCED_SETTINGS': (int, 'General', 0),
|
||||||
'SLACK_ENABLED': (int, 'Slack', 0),
|
'SLACK_ENABLED': (int, 'Slack', 0),
|
||||||
'SLACK_HOOK': (str, 'Slack', ''),
|
'SLACK_HOOK': (str, 'Slack', ''),
|
||||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||||
@@ -610,7 +613,9 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'XBMC_ON_INTUP': (int, 'XBMC', 0),
|
'XBMC_ON_INTUP': (int, 'XBMC', 0),
|
||||||
'XBMC_ON_PMSUPDATE': (int, 'XBMC', 0),
|
'XBMC_ON_PMSUPDATE': (int, 'XBMC', 0),
|
||||||
'XBMC_ON_CONCURRENT': (int, 'XBMC', 0),
|
'XBMC_ON_CONCURRENT': (int, 'XBMC', 0),
|
||||||
'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0)
|
'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0),
|
||||||
|
'JWT_SECRET': (str, 'Advanced', ''),
|
||||||
|
'SYSTEM_ANALYTICS': (int, 'Advanced', 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
_BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID', '_HOOK']
|
_BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID', '_HOOK']
|
||||||
@@ -873,3 +878,15 @@ class Config(object):
|
|||||||
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
|
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
|
||||||
|
|
||||||
self.CONFIG_VERSION = 9
|
self.CONFIG_VERSION = 9
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == 9:
|
||||||
|
if self.PMS_UPDATE_CHANNEL == 'plexpass':
|
||||||
|
self.PMS_UPDATE_CHANNEL = 'beta'
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = 10
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == 10:
|
||||||
|
self.GIT_USER = 'Tautulli'
|
||||||
|
self.GIT_REPO = 'Tautulli'
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = 11
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import time
|
|||||||
import plexpy
|
import plexpy
|
||||||
import logger
|
import logger
|
||||||
|
|
||||||
FILENAME = "plexpy.db"
|
FILENAME = "tautulli.db"
|
||||||
db_lock = threading.Lock()
|
db_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
@@ -63,9 +63,9 @@ def make_backup(cleanup=False, scheduler=False):
|
|||||||
""" Makes a backup of db, removes all but the last 5 backups """
|
""" Makes a backup of db, removes all but the last 5 backups """
|
||||||
|
|
||||||
if scheduler:
|
if scheduler:
|
||||||
backup_file = 'plexpy.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
|
backup_file = 'tautulli.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
|
||||||
else:
|
else:
|
||||||
backup_file = 'plexpy.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
|
backup_file = 'tautulli.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
|
||||||
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
||||||
backup_file_fp = os.path.join(backup_folder, backup_file)
|
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ class DataFactory(object):
|
|||||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
'photo': 0,
|
'photo': 0,
|
||||||
'clip': plexpy.CONFIG.MOVIE_WATCHED_PERCENT
|
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||||
}
|
}
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
@@ -612,7 +612,6 @@ class DataFactory(object):
|
|||||||
'total_plays': item['total_plays'],
|
'total_plays': item['total_plays'],
|
||||||
'total_duration': item['total_duration'],
|
'total_duration': item['total_duration'],
|
||||||
'last_play': item['last_watch'],
|
'last_play': item['last_watch'],
|
||||||
'thumb': user_thumb,
|
|
||||||
'user_thumb': user_thumb,
|
'user_thumb': user_thumb,
|
||||||
'grandparent_thumb': '',
|
'grandparent_thumb': '',
|
||||||
'art': '',
|
'art': '',
|
||||||
@@ -827,6 +826,9 @@ class DataFactory(object):
|
|||||||
if session.get_session_shared_libraries():
|
if session.get_session_shared_libraries():
|
||||||
library_cards = session.get_session_shared_libraries()
|
library_cards = session.get_session_shared_libraries()
|
||||||
|
|
||||||
|
if 'first_run_wizard' in library_cards:
|
||||||
|
return None
|
||||||
|
|
||||||
library_stats = []
|
library_stats = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -951,9 +953,11 @@ class DataFactory(object):
|
|||||||
'transcode_hw_encoding': item['transcode_hw_encoding'],
|
'transcode_hw_encoding': item['transcode_hw_encoding'],
|
||||||
'media_type': item['media_type'],
|
'media_type': item['media_type'],
|
||||||
'title': item['title'],
|
'title': item['title'],
|
||||||
'grandparent_title': item['grandparent_title']
|
'grandparent_title': item['grandparent_title'],
|
||||||
|
'current_session': 1 if session_key else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream_output = {k: v or '' for k, v in stream_output.iteritems()}
|
||||||
return stream_output
|
return stream_output
|
||||||
|
|
||||||
def get_metadata_details(self, rating_key):
|
def get_metadata_details(self, rating_key):
|
||||||
@@ -1105,6 +1109,7 @@ class DataFactory(object):
|
|||||||
def get_poster_info(self, rating_key='', metadata=None):
|
def get_poster_info(self, rating_key='', metadata=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
poster_key = ''
|
||||||
if str(rating_key).isdigit():
|
if str(rating_key).isdigit():
|
||||||
poster_key = rating_key
|
poster_key = rating_key
|
||||||
elif metadata:
|
elif metadata:
|
||||||
@@ -1116,6 +1121,7 @@ class DataFactory(object):
|
|||||||
poster_key = metadata['parent_rating_key']
|
poster_key = metadata['parent_rating_key']
|
||||||
|
|
||||||
poster_info = {}
|
poster_info = {}
|
||||||
|
|
||||||
if poster_key:
|
if poster_key:
|
||||||
try:
|
try:
|
||||||
query = 'SELECT poster_title, poster_url FROM poster_urls ' \
|
query = 'SELECT poster_title, poster_url FROM poster_urls ' \
|
||||||
@@ -1126,14 +1132,15 @@ class DataFactory(object):
|
|||||||
|
|
||||||
return poster_info
|
return poster_info
|
||||||
|
|
||||||
def set_poster_url(self, rating_key='', poster_title='', poster_url=''):
|
def set_poster_url(self, rating_key='', poster_title='', poster_url='', delete_hash=''):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if str(rating_key).isdigit():
|
if str(rating_key).isdigit():
|
||||||
keys = {'rating_key': int(rating_key)}
|
keys = {'rating_key': int(rating_key)}
|
||||||
|
|
||||||
values = {'poster_title': poster_title,
|
values = {'poster_title': poster_title,
|
||||||
'poster_url': poster_url}
|
'poster_url': poster_url,
|
||||||
|
'delete_hash': delete_hash}
|
||||||
|
|
||||||
monitor_db.upsert(table_name='poster_urls', key_dict=keys, value_dict=values)
|
monitor_db.upsert(table_name='poster_urls', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
@@ -1141,10 +1148,62 @@ class DataFactory(object):
|
|||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if rating_key:
|
if rating_key:
|
||||||
logger.info(u"Tautulli DataFactory :: Deleting poster_url for rating_key %s from the database." % rating_key)
|
poster_info = monitor_db.select_single('SELECT poster_title, delete_hash '
|
||||||
|
'FROM poster_urls WHERE rating_key = ?',
|
||||||
|
[rating_key])
|
||||||
|
if poster_info['delete_hash']:
|
||||||
|
helpers.delete_from_imgur(poster_info['delete_hash'], poster_info['poster_title'])
|
||||||
|
|
||||||
|
logger.info(u"Tautulli DataFactory :: Deleting poster_url for '%s' (rating_key %s) from the database."
|
||||||
|
% (poster_info['poster_title'], rating_key))
|
||||||
result = monitor_db.action('DELETE FROM poster_urls WHERE rating_key = ?', [rating_key])
|
result = monitor_db.action('DELETE FROM poster_urls WHERE rating_key = ?', [rating_key])
|
||||||
return True if result else False
|
return True if result else False
|
||||||
|
|
||||||
|
def get_lookup_info(self, rating_key='', metadata=None):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
lookup_key = ''
|
||||||
|
if str(rating_key).isdigit():
|
||||||
|
lookup_key = rating_key
|
||||||
|
elif metadata:
|
||||||
|
if metadata['media_type'] in ('movie', 'show', 'artist'):
|
||||||
|
lookup_key = metadata['rating_key']
|
||||||
|
elif metadata['media_type'] in ('season', 'album'):
|
||||||
|
lookup_key = metadata['parent_rating_key']
|
||||||
|
elif metadata['media_type'] in ('episode', 'track'):
|
||||||
|
lookup_key = metadata['grandparent_rating_key']
|
||||||
|
|
||||||
|
lookup_info = {'tvmaze_id': '',
|
||||||
|
'themoviedb_id': ''}
|
||||||
|
|
||||||
|
if lookup_key:
|
||||||
|
try:
|
||||||
|
query = 'SELECT tvmaze_id FROM tvmaze_lookup ' \
|
||||||
|
'WHERE rating_key = ?'
|
||||||
|
tvmaze_info = monitor_db.select_single(query, args=[lookup_key])
|
||||||
|
if tvmaze_info:
|
||||||
|
lookup_info['tvmaze_id'] = tvmaze_info['tvmaze_id']
|
||||||
|
|
||||||
|
query = 'SELECT themoviedb_id FROM themoviedb_lookup ' \
|
||||||
|
'WHERE rating_key = ?'
|
||||||
|
themoviedb_info = monitor_db.select_single(query, args=[lookup_key])
|
||||||
|
if themoviedb_info:
|
||||||
|
lookup_info['themoviedb_id'] = themoviedb_info['themoviedb_id']
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Tautulli DataFactory :: Unable to execute database query for get_lookup_info: %s." % e)
|
||||||
|
|
||||||
|
return lookup_info
|
||||||
|
|
||||||
|
def delete_lookup_info(self, rating_key='', title=''):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
if rating_key:
|
||||||
|
logger.info(u"Tautulli DataFactory :: Deleting lookup info for '%s' (rating_key %s) from the database."
|
||||||
|
% (title, rating_key))
|
||||||
|
result_tvmaze = monitor_db.action('DELETE FROM tvmaze_lookup WHERE rating_key = ?', [rating_key])
|
||||||
|
result_themoviedb = monitor_db.action('DELETE FROM themoviedb_lookup WHERE rating_key = ?', [rating_key])
|
||||||
|
return True if (result_tvmaze or result_themoviedb) else False
|
||||||
|
|
||||||
def get_search_query(self, rating_key=''):
|
def get_search_query(self, rating_key=''):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
|||||||
228
plexpy/graphs.py
228
plexpy/graphs.py
@@ -27,7 +27,7 @@ class Graphs(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_total_plays_per_day(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_per_day(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -38,17 +38,22 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
|
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
|
||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY date_played ' \
|
'GROUP BY date_played ' \
|
||||||
'ORDER BY started ASC' % (time_range, user_cond)
|
'ORDER BY started ASC' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -60,7 +65,7 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY date_played ' \
|
'GROUP BY date_played ' \
|
||||||
'ORDER BY started ASC' % (time_range, user_cond)
|
'ORDER BY started ASC' % (time_range, user_cond)
|
||||||
|
|
||||||
@@ -111,7 +116,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_per_dayofweek(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_per_dayofweek(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -122,7 +127,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT strftime("%%w", datetime(started, "unixepoch", "localtime")) AS daynumber, ' \
|
query = 'SELECT strftime("%%w", datetime(started, "unixepoch", "localtime")) AS daynumber, ' \
|
||||||
@@ -137,10 +147,10 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY dayofweek ' \
|
'GROUP BY dayofweek ' \
|
||||||
'ORDER BY daynumber' % (time_range, user_cond)
|
'ORDER BY daynumber' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -160,7 +170,7 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY dayofweek ' \
|
'GROUP BY dayofweek ' \
|
||||||
'ORDER BY daynumber' % (time_range, user_cond)
|
'ORDER BY daynumber' % (time_range, user_cond)
|
||||||
|
|
||||||
@@ -212,7 +222,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_per_hourofday(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_per_hourofday(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -223,17 +233,22 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT strftime("%%H", datetime(started, "unixepoch", "localtime")) AS hourofday, ' \
|
query = 'SELECT strftime("%%H", datetime(started, "unixepoch", "localtime")) AS hourofday, ' \
|
||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY hourofday ' \
|
'GROUP BY hourofday ' \
|
||||||
'ORDER BY hourofday' % (time_range, user_cond)
|
'ORDER BY hourofday' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -245,7 +260,7 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
|
||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||||
'GROUP BY hourofday ' \
|
'GROUP BY hourofday ' \
|
||||||
'ORDER BY hourofday' % (time_range, user_cond)
|
'ORDER BY hourofday' % (time_range, user_cond)
|
||||||
|
|
||||||
@@ -295,9 +310,9 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None):
|
def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, grouping=None):
|
||||||
import time as time
|
import time as time
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
time_range = '12'
|
time_range = '12'
|
||||||
|
|
||||||
@@ -308,17 +323,22 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) AS datestring, ' \
|
query = 'SELECT strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) AS datestring, ' \
|
||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
||||||
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
|
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
|
||||||
'ORDER BY datestring DESC LIMIT %s' % (time_range, user_cond, time_range)
|
'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -384,7 +404,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -395,7 +415,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT platform, ' \
|
query = 'SELECT platform, ' \
|
||||||
@@ -403,11 +428,11 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \
|
||||||
'COUNT(id) AS total_count ' \
|
'COUNT(id) AS total_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||||
'GROUP BY platform ' \
|
'GROUP BY platform ' \
|
||||||
'ORDER BY total_count DESC ' \
|
'ORDER BY total_count DESC ' \
|
||||||
'LIMIT 10' % (time_range, user_cond)
|
'LIMIT 10' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -421,7 +446,7 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
|
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
|
||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||||
'GROUP BY platform ' \
|
'GROUP BY platform ' \
|
||||||
'ORDER BY total_duration DESC ' \
|
'ORDER BY total_duration DESC ' \
|
||||||
'LIMIT 10' % (time_range, user_cond)
|
'LIMIT 10' % (time_range, user_cond)
|
||||||
@@ -453,7 +478,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -464,7 +489,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT ' \
|
query = 'SELECT ' \
|
||||||
@@ -475,12 +505,12 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \
|
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \
|
||||||
'COUNT(session_history.id) AS total_count ' \
|
'COUNT(session_history.id) AS total_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||||
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||||
'GROUP BY session_history.user_id ' \
|
'GROUP BY session_history.user_id ' \
|
||||||
'ORDER BY total_count DESC ' \
|
'ORDER BY total_count DESC ' \
|
||||||
'LIMIT 10' % (time_range, user_cond)
|
'LIMIT 10' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -498,7 +528,7 @@ class Graphs(object):
|
|||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||||
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
|
||||||
'GROUP BY session_history.user_id ' \
|
'GROUP BY session_history.user_id ' \
|
||||||
'ORDER BY total_duration DESC ' \
|
'ORDER BY total_duration DESC ' \
|
||||||
'LIMIT 10' % (time_range, user_cond)
|
'LIMIT 10' % (time_range, user_cond)
|
||||||
@@ -535,7 +565,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_per_stream_type(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_per_stream_type(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -546,7 +576,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT date(session_history.started, "unixepoch", "localtime") AS date_played, ' \
|
query = 'SELECT date(session_history.started, "unixepoch", "localtime") AS date_played, ' \
|
||||||
@@ -556,14 +591,15 @@ class Graphs(object):
|
|||||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||||
'THEN 1 ELSE 0 END) AS tc_count ' \
|
'THEN 1 ELSE 0 END) AS tc_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \
|
'(session_history.media_type = "episode" OR ' \
|
||||||
|
'session_history.media_type = "movie" OR ' \
|
||||||
'session_history.media_type = "track") %s' \
|
'session_history.media_type = "track") %s' \
|
||||||
'GROUP BY date_played ' \
|
'GROUP BY date_played ' \
|
||||||
'ORDER BY started ASC' % (time_range, user_cond)
|
'ORDER BY started ASC' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -579,7 +615,7 @@ class Graphs(object):
|
|||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime") AND ' \
|
'datetime("now", "-%s days", "localtime") AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \
|
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \
|
||||||
'session_history.media_type = "track") %s' \
|
'session_history.media_type = "track") %s' \
|
||||||
@@ -633,7 +669,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -644,7 +680,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT session_history_media_info.video_resolution AS resolution, ' \
|
query = 'SELECT session_history_media_info.video_resolution AS resolution, ' \
|
||||||
@@ -655,14 +696,14 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||||
'COUNT(session_history.id) AS total_count ' \
|
'COUNT(session_history.id) AS total_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
||||||
'GROUP BY resolution ' \
|
'GROUP BY resolution ' \
|
||||||
'ORDER BY total_count DESC ' \
|
'ORDER BY total_count DESC ' \
|
||||||
'LIMIT 10' % (time_range, user_cond)
|
'LIMIT 10' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -680,7 +721,7 @@ class Graphs(object):
|
|||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
||||||
'GROUP BY resolution ' \
|
'GROUP BY resolution ' \
|
||||||
@@ -698,6 +739,10 @@ class Graphs(object):
|
|||||||
series_3 = []
|
series_3 = []
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
|
if item['resolution'] not in ('4k', 'unknown'):
|
||||||
|
item['resolution'] = item['resolution'].upper()
|
||||||
|
if item['resolution'].isdigit():
|
||||||
|
item['resolution'] += 'p'
|
||||||
categories.append(item['resolution'])
|
categories.append(item['resolution'])
|
||||||
series_1.append(item['dp_count'])
|
series_1.append(item['dp_count'])
|
||||||
series_2.append(item['ds_count'])
|
series_2.append(item['ds_count'])
|
||||||
@@ -714,7 +759,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_total_plays_by_stream_resolution(self, time_range='30', y_axis='plays', user_id=None):
|
def get_total_plays_by_stream_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -725,20 +770,27 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT ' \
|
query = 'SELECT ' \
|
||||||
|
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \
|
||||||
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
||||||
'(CASE ' \
|
'(CASE ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 360 THEN "sd" ' \
|
'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
|
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
|
||||||
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
|
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) ' \
|
||||||
|
'ELSE session_history_media_info.stream_video_resolution END) AS resolution, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||||
@@ -746,28 +798,30 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\
|
||||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||||
'COUNT(session_history.id) AS total_count ' \
|
'COUNT(session_history.id) AS total_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
||||||
'GROUP BY resolution ' \
|
'GROUP BY resolution ' \
|
||||||
'ORDER BY total_count DESC ' \
|
'ORDER BY total_count DESC ' \
|
||||||
'LIMIT 10' % (time_range, user_cond)
|
'LIMIT 10' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
query = 'SELECT ' \
|
query = 'SELECT ' \
|
||||||
|
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \
|
||||||
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
||||||
'(CASE ' \
|
'(CASE ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 360 THEN "sd" ' \
|
'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
|
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
|
||||||
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
|
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) ' \
|
||||||
|
'ELSE session_history_media_info.stream_video_resolution END) AS resolution, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||||
@@ -781,7 +835,7 @@ class Graphs(object):
|
|||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
|
||||||
'GROUP BY resolution ' \
|
'GROUP BY resolution ' \
|
||||||
@@ -799,6 +853,10 @@ class Graphs(object):
|
|||||||
series_3 = []
|
series_3 = []
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
|
if item['resolution'] not in ('4k', 'unknown'):
|
||||||
|
item['resolution'] = item['resolution'].upper()
|
||||||
|
if item['resolution'].isdigit():
|
||||||
|
item['resolution'] += 'p'
|
||||||
categories.append(item['resolution'])
|
categories.append(item['resolution'])
|
||||||
series_1.append(item['dp_count'])
|
series_1.append(item['dp_count'])
|
||||||
series_2.append(item['ds_count'])
|
series_2.append(item['ds_count'])
|
||||||
@@ -815,7 +873,7 @@ class Graphs(object):
|
|||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None):
|
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -826,7 +884,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT session_history.platform AS platform, ' \
|
query = 'SELECT session_history.platform AS platform, ' \
|
||||||
@@ -837,13 +900,15 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||||
'COUNT(session_history.id) AS total_count ' \
|
'COUNT(session_history.id) AS total_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime") AND ' \
|
'datetime("now", "-%s days", "localtime") AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \
|
'(session_history.media_type = "episode" OR ' \
|
||||||
|
'session_history.media_type = "movie" OR ' \
|
||||||
|
'session_history.media_type = "track") %s' \
|
||||||
'GROUP BY platform ' \
|
'GROUP BY platform ' \
|
||||||
'ORDER BY total_count DESC LIMIT 10' % (time_range, user_cond)
|
'ORDER BY total_count DESC LIMIT 10' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -862,9 +927,11 @@ class Graphs(object):
|
|||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
|
||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime") AND ' \
|
'datetime("now", "-%s days", "localtime") AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \
|
'(session_history.media_type = "episode" OR ' \
|
||||||
|
'session_history.media_type = "movie" OR ' \
|
||||||
|
'session_history.media_type = "track") %s' \
|
||||||
'GROUP BY platform ' \
|
'GROUP BY platform ' \
|
||||||
'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond)
|
'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond)
|
||||||
|
|
||||||
@@ -896,7 +963,7 @@ class Graphs(object):
|
|||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None):
|
def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
@@ -907,7 +974,12 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
elif user_id and user_id.isdigit():
|
elif user_id and user_id.isdigit():
|
||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
|
if grouping is None:
|
||||||
|
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT ' \
|
query = 'SELECT ' \
|
||||||
@@ -921,14 +993,16 @@ class Graphs(object):
|
|||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||||
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
'THEN 1 ELSE 0 END) AS tc_count, ' \
|
||||||
'COUNT(session_history.id) AS total_count ' \
|
'COUNT(session_history.id) AS total_count ' \
|
||||||
'FROM session_history ' \
|
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime") AND ' \
|
'datetime("now", "-%s days", "localtime") AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \
|
'(session_history.media_type = "episode" OR ' \
|
||||||
|
'session_history.media_type = "movie" OR ' \
|
||||||
|
'session_history.media_type = "track") %s' \
|
||||||
'GROUP BY username ' \
|
'GROUP BY username ' \
|
||||||
'ORDER BY total_count DESC LIMIT 10' % (time_range, user_cond)
|
'ORDER BY total_count DESC LIMIT 10' % (group_by, time_range, user_cond)
|
||||||
|
|
||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
@@ -951,9 +1025,11 @@ class Graphs(object):
|
|||||||
'FROM session_history ' \
|
'FROM session_history ' \
|
||||||
'JOIN users ON session_history.user_id = users.user_id ' \
|
'JOIN users ON session_history.user_id = users.user_id ' \
|
||||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||||
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
|
'WHERE datetime(started, "unixepoch", "localtime") >= ' \
|
||||||
'datetime("now", "-%s days", "localtime") AND ' \
|
'datetime("now", "-%s days", "localtime") AND ' \
|
||||||
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \
|
'(session_history.media_type = "episode" OR ' \
|
||||||
|
'session_history.media_type = "movie" OR ' \
|
||||||
|
'session_history.media_type = "track") %s' \
|
||||||
'GROUP BY username ' \
|
'GROUP BY username ' \
|
||||||
'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond)
|
'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond)
|
||||||
|
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ def whois_lookup(ip_address):
|
|||||||
countries = ipwhois.utils.get_countries()
|
countries = ipwhois.utils.get_countries()
|
||||||
nets = whois['nets']
|
nets = whois['nets']
|
||||||
for net in nets:
|
for net in nets:
|
||||||
net['country'] = countries[net['country']]
|
net['country'] = countries.get(net['country'])
|
||||||
if net['postal_code']:
|
if net['postal_code']:
|
||||||
net['postal_code'] = net['postal_code'].replace('-', ' ')
|
net['postal_code'] = net['postal_code'].replace('-', ' ')
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -680,21 +680,21 @@ def anon_url(*url):
|
|||||||
"""
|
"""
|
||||||
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
|
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
|
||||||
|
|
||||||
def uploadToImgur(imgPath, imgTitle=''):
|
def upload_to_imgur(imgPath, imgTitle=''):
|
||||||
""" Uploads an image to Imgur """
|
""" Uploads an image to Imgur """
|
||||||
client_id = plexpy.CONFIG.IMGUR_CLIENT_ID
|
client_id = plexpy.CONFIG.IMGUR_CLIENT_ID
|
||||||
img_url = ''
|
img_url = delete_hash = ''
|
||||||
|
|
||||||
if not client_id:
|
if not client_id:
|
||||||
logger.error(u"Tautulli Helpers :: Cannot upload poster to Imgur. No Imgur client id specified in the settings.")
|
logger.error(u"Tautulli Helpers :: Cannot upload poster to Imgur. No Imgur client id specified in the settings.")
|
||||||
return img_url
|
return img_url, delete_hash
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(imgPath, 'rb') as imgFile:
|
with open(imgPath, 'rb') as imgFile:
|
||||||
img = imgFile.read()
|
img = imgFile.read()
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(u"Tautulli Helpers :: Unable to read image file for Imgur: %s" % e)
|
logger.error(u"Tautulli Helpers :: Unable to read image file for Imgur: %s" % e)
|
||||||
return img_url
|
return img_url, delete_hash
|
||||||
|
|
||||||
headers = {'Authorization': 'Client-ID %s' % client_id}
|
headers = {'Authorization': 'Client-ID %s' % client_id}
|
||||||
data = {'type': 'base64',
|
data = {'type': 'base64',
|
||||||
@@ -703,13 +703,15 @@ def uploadToImgur(imgPath, imgTitle=''):
|
|||||||
data['title'] = imgTitle.encode('utf-8')
|
data['title'] = imgTitle.encode('utf-8')
|
||||||
data['name'] = imgTitle.encode('utf-8') + '.jpg'
|
data['name'] = imgTitle.encode('utf-8') + '.jpg'
|
||||||
|
|
||||||
response, err_msg, req_msg = request.request_response2('https://api.imgur.com/3/image', 'POST', headers=headers, data=data)
|
response, err_msg, req_msg = request.request_response2('https://api.imgur.com/3/image', 'POST',
|
||||||
|
headers=headers, data=data)
|
||||||
|
|
||||||
if response and not err_msg:
|
if response and not err_msg:
|
||||||
t = '\'' + imgTitle + '\' ' if imgTitle else ''
|
t = '\'' + imgTitle + '\' ' if imgTitle else ''
|
||||||
logger.debug(u"Tautulli Helpers :: Image {}uploaded to Imgur.".format(t))
|
logger.debug(u"Tautulli Helpers :: Image {}uploaded to Imgur.".format(t))
|
||||||
img_url = response.json().get('data').get('link', '').replace('http://', 'https://')
|
imgur_response_data = response.json().get('data')
|
||||||
|
img_url = imgur_response_data.get('link', '').replace('http://', 'https://')
|
||||||
|
delete_hash = imgur_response_data.get('deletehash', '')
|
||||||
else:
|
else:
|
||||||
if err_msg:
|
if err_msg:
|
||||||
logger.error(u"Tautulli Helpers :: Unable to upload image to Imgur: {}".format(err_msg))
|
logger.error(u"Tautulli Helpers :: Unable to upload image to Imgur: {}".format(err_msg))
|
||||||
@@ -719,7 +721,27 @@ def uploadToImgur(imgPath, imgTitle=''):
|
|||||||
if req_msg:
|
if req_msg:
|
||||||
logger.debug(u"Tautulli Helpers :: Request response: {}".format(req_msg))
|
logger.debug(u"Tautulli Helpers :: Request response: {}".format(req_msg))
|
||||||
|
|
||||||
return img_url
|
return img_url, delete_hash
|
||||||
|
|
||||||
|
def delete_from_imgur(delete_hash, imgTitle=''):
|
||||||
|
""" Deletes an image from Imgur """
|
||||||
|
client_id = plexpy.CONFIG.IMGUR_CLIENT_ID
|
||||||
|
|
||||||
|
headers = {'Authorization': 'Client-ID %s' % client_id}
|
||||||
|
|
||||||
|
response, err_msg, req_msg = request.request_response2('https://api.imgur.com/3/image/%s' % delete_hash, 'DELETE',
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
if response and not err_msg:
|
||||||
|
t = '\'' + imgTitle + '\' ' if imgTitle else ''
|
||||||
|
logger.debug(u"Tautulli Helpers :: Image {}deleted from Imgur.".format(t))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if err_msg:
|
||||||
|
logger.error(u"Tautulli Helpers :: Unable to delete image from Imgur: {}".format(err_msg))
|
||||||
|
else:
|
||||||
|
logger.error(u"Tautulli Helpers :: Unable to delete image from Imgur.")
|
||||||
|
return False
|
||||||
|
|
||||||
def cache_image(url, image=None):
|
def cache_image(url, image=None):
|
||||||
"""
|
"""
|
||||||
@@ -911,3 +933,36 @@ def eval_logic_groups_to_bool(logic_groups, eval_conds):
|
|||||||
result = result or eval_cond
|
result = result or eval_cond
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_plexpy_url(hostname=None):
|
||||||
|
if plexpy.CONFIG.ENABLE_HTTPS:
|
||||||
|
scheme = 'https'
|
||||||
|
else:
|
||||||
|
scheme = 'http'
|
||||||
|
|
||||||
|
if hostname is None and plexpy.CONFIG.HTTP_HOST == '0.0.0.0':
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||||
|
s.connect(('<broadcast>', 0))
|
||||||
|
hostname = s.getsockname()[0]
|
||||||
|
except socket.error:
|
||||||
|
hostname = socket.gethostbyname(socket.gethostname())
|
||||||
|
|
||||||
|
if not hostname:
|
||||||
|
hostname = 'localhost'
|
||||||
|
else:
|
||||||
|
hostname = hostname or plexpy.CONFIG.HTTP_HOST
|
||||||
|
|
||||||
|
if plexpy.CONFIG.HTTP_PORT not in (80, 443):
|
||||||
|
port = ':' + str(plexpy.CONFIG.HTTP_PORT)
|
||||||
|
else:
|
||||||
|
port = ''
|
||||||
|
|
||||||
|
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
|
||||||
|
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/')
|
||||||
|
else:
|
||||||
|
root = ''
|
||||||
|
|
||||||
|
return scheme + '://' + hostname + port + root
|
||||||
@@ -39,11 +39,17 @@ class HTTPHandler(object):
|
|||||||
else:
|
else:
|
||||||
self.urls = urls
|
self.urls = urls
|
||||||
|
|
||||||
|
self.headers = {'X-Plex-Device-Name': 'Tautulli',
|
||||||
|
'X-Plex-Product': 'Tautulli',
|
||||||
|
'X-Plex-Version': plexpy.common.RELEASE,
|
||||||
|
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||||
|
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
||||||
|
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||||
|
}
|
||||||
|
|
||||||
self.token = token
|
self.token = token
|
||||||
if self.token:
|
if self.token:
|
||||||
self.headers = {'X-Plex-Token': self.token}
|
self.headers['X-Plex-Token'] = self.token
|
||||||
else:
|
|
||||||
self.headers = {}
|
|
||||||
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.ssl_verify = ssl_verify
|
self.ssl_verify = ssl_verify
|
||||||
@@ -65,7 +71,7 @@ class HTTPHandler(object):
|
|||||||
Output: list
|
Output: list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.uri = uri
|
self.uri = uri.encode('utf-8')
|
||||||
self.request_type = request_type.upper()
|
self.request_type = request_type.upper()
|
||||||
self.output_format = output_format.lower()
|
self.output_format = output_format.lower()
|
||||||
self.return_type = return_type
|
self.return_type = return_type
|
||||||
@@ -79,9 +85,9 @@ class HTTPHandler(object):
|
|||||||
if uri:
|
if uri:
|
||||||
request_urls = [urljoin(url, self.uri) for url in self.urls]
|
request_urls = [urljoin(url, self.uri) for url in self.urls]
|
||||||
|
|
||||||
if no_token and headers:
|
if no_token:
|
||||||
self.headers = headers
|
self.headers.pop('X-Plex-Token', None)
|
||||||
elif headers:
|
if headers:
|
||||||
self.headers.update(headers)
|
self.headers.update(headers)
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ def update_section_ids():
|
|||||||
library_children = pms_connect.get_library_children_details(section_id=section_id,
|
library_children = pms_connect.get_library_children_details(section_id=section_id,
|
||||||
section_type=section_type)
|
section_type=section_type)
|
||||||
if library_children:
|
if library_children:
|
||||||
children_list = library_children['childern_list']
|
children_list = library_children['children_list']
|
||||||
key_mappings.update({child['rating_key']:child['section_id'] for child in children_list})
|
key_mappings.update({child['rating_key']: child['section_id'] for child in children_list})
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id)
|
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id)
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ def update_labels():
|
|||||||
label_key=label['label_key'])
|
label_key=label['label_key'])
|
||||||
|
|
||||||
if library_children:
|
if library_children:
|
||||||
children_list = library_children['childern_list']
|
children_list = library_children['children_list']
|
||||||
# rating_key_list = [child['rating_key'] for child in children_list]
|
# rating_key_list = [child['rating_key'] for child in children_list]
|
||||||
|
|
||||||
for rating_key in [child['rating_key'] for child in children_list]:
|
for rating_key in [child['rating_key'] for child in children_list]:
|
||||||
@@ -456,7 +456,7 @@ class Libraries(object):
|
|||||||
get_media_info=True)
|
get_media_info=True)
|
||||||
if library_children:
|
if library_children:
|
||||||
library_count = library_children['library_count']
|
library_count = library_children['library_count']
|
||||||
children_list = library_children['childern_list']
|
children_list = library_children['children_list']
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items.")
|
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items.")
|
||||||
return default_return
|
return default_return
|
||||||
@@ -544,19 +544,19 @@ class Libraries(object):
|
|||||||
filtered_count = len(results)
|
filtered_count = len(results)
|
||||||
|
|
||||||
# Sort results
|
# Sort results
|
||||||
results = sorted(results, key=lambda k: k['sort_title'])
|
results = sorted(results, key=lambda k: k['sort_title'].lower())
|
||||||
sort_order = json_data['order']
|
sort_order = json_data['order']
|
||||||
for order in reversed(sort_order):
|
for order in reversed(sort_order):
|
||||||
sort_key = json_data['columns'][int(order['column'])]['data']
|
sort_key = json_data['columns'][int(order['column'])]['data']
|
||||||
reverse = True if order['dir'] == 'desc' else False
|
reverse = True if order['dir'] == 'desc' else False
|
||||||
if rating_key and sort_key == 'sort_title':
|
if rating_key and sort_key == 'sort_title':
|
||||||
results = sorted(results, key=lambda k: helpers.cast_to_int(k['media_index']), reverse=reverse)
|
results = sorted(results, key=lambda k: helpers.cast_to_int(k['media_index']), reverse=reverse)
|
||||||
elif sort_key == 'file_size' or sort_key == 'bitrate':
|
elif sort_key in ('file_size', 'bitrate', 'added_at', 'last_played', 'play_count'):
|
||||||
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse)
|
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse)
|
||||||
elif sort_key == 'video_resolution':
|
elif sort_key == 'video_resolution':
|
||||||
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse)
|
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse)
|
||||||
else:
|
else:
|
||||||
results = sorted(results, key=lambda k: k[sort_key], reverse=reverse)
|
results = sorted(results, key=lambda k: k[sort_key].lower(), reverse=reverse)
|
||||||
|
|
||||||
total_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results])
|
total_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results])
|
||||||
|
|
||||||
@@ -744,7 +744,7 @@ class Libraries(object):
|
|||||||
logger.warn(u"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
|
logger.warn(u"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
|
||||||
% section_id)
|
% section_id)
|
||||||
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
|
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
|
||||||
pmsconnect.refresh_libraries()
|
refresh_libraries()
|
||||||
|
|
||||||
library_details = get_library_details(section_id=section_id)
|
library_details = get_library_details(section_id=section_id)
|
||||||
|
|
||||||
@@ -1006,13 +1006,13 @@ class Libraries(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)
|
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)
|
||||||
|
|
||||||
def delete_datatable_media_info_cache(self, section_id=None):
|
def delete_media_info_cache(self, section_id=None):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if section_id.isdigit():
|
if section_id.isdigit():
|
||||||
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
||||||
if f.startswith('media_info-%s' % section_id) and f.endswith('.json')]
|
if f.startswith('media_info_%s' % section_id) and f.endswith('.json')]
|
||||||
|
|
||||||
logger.debug(u"Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
logger.debug(u"Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
||||||
return 'Deleted media info table cache for library with id %s.' % section_id
|
return 'Deleted media info table cache for library with id %s.' % section_id
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ import plexpy
|
|||||||
import helpers
|
import helpers
|
||||||
|
|
||||||
# These settings are for file logging only
|
# These settings are for file logging only
|
||||||
FILENAME = "plexpy.log"
|
FILENAME = "tautulli.log"
|
||||||
FILENAME_API = "plexpy_api.log"
|
FILENAME_API = "tautulli_api.log"
|
||||||
FILENAME_PLEX_WEBSOCKET = "plex_websocket.log"
|
FILENAME_PLEX_WEBSOCKET = "plex_websocket.log"
|
||||||
MAX_SIZE = 5000000 # 5 MB
|
MAX_SIZE = 5000000 # 5 MB
|
||||||
MAX_FILES = 5
|
MAX_FILES = 5
|
||||||
@@ -39,9 +39,9 @@ MAX_FILES = 5
|
|||||||
_BLACKLIST_WORDS = set()
|
_BLACKLIST_WORDS = set()
|
||||||
|
|
||||||
# Tautulli logger
|
# Tautulli logger
|
||||||
logger = logging.getLogger("plexpy")
|
logger = logging.getLogger("tautulli")
|
||||||
# Tautulli API logger
|
# Tautulli API logger
|
||||||
logger_api = logging.getLogger("plexpy_api")
|
logger_api = logging.getLogger("tautulli_api")
|
||||||
# Tautulli websocket logger
|
# Tautulli websocket logger
|
||||||
logger_plex_websocket = logging.getLogger("plex_websocket")
|
logger_plex_websocket = logging.getLogger("plex_websocket")
|
||||||
|
|
||||||
@@ -178,9 +178,9 @@ def initMultiprocessing():
|
|||||||
def initLogger(console=False, log_dir=False, verbose=False):
|
def initLogger(console=False, log_dir=False, verbose=False):
|
||||||
"""
|
"""
|
||||||
Setup logging for Tautulli. It uses the logger instance with the name
|
Setup logging for Tautulli. It uses the logger instance with the name
|
||||||
'plexpy'. Three log handlers are added:
|
'tautulli'. Three log handlers are added:
|
||||||
|
|
||||||
* RotatingFileHandler: for the file plexpy.log
|
* RotatingFileHandler: for the file tautulli.log
|
||||||
* LogListHandler: for Web UI
|
* LogListHandler: for Web UI
|
||||||
* StreamHandler: for console (if console)
|
* StreamHandler: for console (if console)
|
||||||
|
|
||||||
@@ -306,6 +306,11 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
|
|||||||
# Monkey patch the run() by monkey patching the __init__ method
|
# Monkey patch the run() by monkey patching the __init__ method
|
||||||
threading.Thread.__init__ = new_init
|
threading.Thread.__init__ = new_init
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
logging.shutdown()
|
||||||
|
|
||||||
|
|
||||||
# Expose logger methods
|
# Expose logger methods
|
||||||
# Main Tautulli logger
|
# Main Tautulli logger
|
||||||
info = logger.info
|
info = logger.info
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
|
|||||||
conditions = notify_conditions(notify_action=notify_action,
|
conditions = notify_conditions(notify_action=notify_action,
|
||||||
stream_data=stream_data,
|
stream_data=stream_data,
|
||||||
timeline_data=timeline_data)
|
timeline_data=timeline_data)
|
||||||
|
else:
|
||||||
|
conditions = True
|
||||||
|
|
||||||
if notifiers_enabled and (manual_trigger or conditions):
|
if notifiers_enabled and (manual_trigger or conditions):
|
||||||
if stream_data or timeline_data:
|
if stream_data or timeline_data:
|
||||||
@@ -122,8 +124,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
|
|||||||
|
|
||||||
# Add on_concurrent and on_newdevice to queue if action is on_play
|
# Add on_concurrent and on_newdevice to queue if action is on_play
|
||||||
if notify_action == 'on_play':
|
if notify_action == 'on_play':
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_concurrent'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_newdevice'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_newdevice'})
|
||||||
|
|
||||||
|
|
||||||
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||||
@@ -206,19 +208,21 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||||
|
|
||||||
custom_conditions_logic = notifier_config['custom_conditions_logic']
|
custom_conditions_logic = notifier_config['custom_conditions_logic']
|
||||||
|
custom_conditions = notifier_config['custom_conditions']
|
||||||
|
|
||||||
if custom_conditions_logic:
|
if custom_conditions_logic or any(c for c in custom_conditions if c['value']):
|
||||||
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s." % notifier_id)
|
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s."
|
||||||
|
% notifier_id)
|
||||||
|
|
||||||
custom_conditions = json.loads(notifier_config['custom_conditions'])
|
logic_groups = None
|
||||||
|
if custom_conditions_logic:
|
||||||
try:
|
try:
|
||||||
# Parse and validate the custom conditions logic
|
# Parse and validate the custom conditions logic
|
||||||
logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
|
logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
|
||||||
% (custom_conditions_logic, e))
|
% (custom_conditions_logic, e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
evaluated_conditions = [None] # Set condition {0} to None
|
evaluated_conditions = [None] # Set condition {0} to None
|
||||||
|
|
||||||
@@ -227,10 +231,11 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
operator = condition['operator']
|
operator = condition['operator']
|
||||||
values = condition['value']
|
values = condition['value']
|
||||||
parameter_type = condition['type']
|
parameter_type = condition['type']
|
||||||
|
parameter_value = parameters.get(parameter, "")
|
||||||
|
|
||||||
# Set blank conditions to None
|
# Set blank conditions to True (skip)
|
||||||
if not parameter or not operator or not values:
|
if not parameter or not operator or not values:
|
||||||
evaluated_conditions.append(None)
|
evaluated_conditions.append(True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Make sure the condition values is in a list
|
# Make sure the condition values is in a list
|
||||||
@@ -248,25 +253,25 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
elif parameter_type == 'float':
|
elif parameter_type == 'float':
|
||||||
values = [float(v) for v in values]
|
values = [float(v) for v in values]
|
||||||
|
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s' to type '%s'."
|
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s', values '%s', to type '%s'."
|
||||||
% (parameter, parameter_type))
|
% (parameter, values, parameter_type))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Cast the parameter value to the correct type
|
# Cast the parameter value to the correct type
|
||||||
try:
|
try:
|
||||||
if parameter_type == 'str':
|
if parameter_type == 'str':
|
||||||
parameter_value = unicode(parameters[parameter]).lower()
|
parameter_value = unicode(parameter_value).lower()
|
||||||
|
|
||||||
elif parameter_type == 'int':
|
elif parameter_type == 'int':
|
||||||
parameter_value = int(parameters[parameter])
|
parameter_value = int(parameter_value)
|
||||||
|
|
||||||
elif parameter_type == 'float':
|
elif parameter_type == 'float':
|
||||||
parameter_value = float(parameters[parameter])
|
parameter_value = float(parameter_value)
|
||||||
|
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s' to type '%s'."
|
logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s', value '%s', to type '%s'."
|
||||||
% (parameter, parameter_type))
|
% (parameter, parameter_value, parameter_type))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check each condition
|
# Check each condition
|
||||||
@@ -274,13 +279,13 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
evaluated_conditions.append(any(c in parameter_value for c in values))
|
evaluated_conditions.append(any(c in parameter_value for c in values))
|
||||||
|
|
||||||
elif operator == 'does not contain':
|
elif operator == 'does not contain':
|
||||||
evaluated_conditions.append(any(c not in parameter_value for c in values))
|
evaluated_conditions.append(all(c not in parameter_value for c in values))
|
||||||
|
|
||||||
elif operator == 'is':
|
elif operator == 'is':
|
||||||
evaluated_conditions.append(any(parameter_value == c for c in values))
|
evaluated_conditions.append(any(parameter_value == c for c in values))
|
||||||
|
|
||||||
elif operator == 'is not':
|
elif operator == 'is not':
|
||||||
evaluated_conditions.append(any(parameter_value != c for c in values))
|
evaluated_conditions.append(all(parameter_value != c for c in values))
|
||||||
|
|
||||||
elif operator == 'begins with':
|
elif operator == 'begins with':
|
||||||
evaluated_conditions.append(parameter_value.startswith(tuple(values)))
|
evaluated_conditions.append(parameter_value.startswith(tuple(values)))
|
||||||
@@ -298,12 +303,15 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
logger.warn(u"Tautulli NotificationHandler :: Invalid condition operator '%s'." % operator)
|
logger.warn(u"Tautulli NotificationHandler :: Invalid condition operator '%s'." % operator)
|
||||||
evaluated_conditions.append(None)
|
evaluated_conditions.append(None)
|
||||||
|
|
||||||
# Format and evaluate the logic string
|
if logic_groups:
|
||||||
try:
|
# Format and evaluate the logic string
|
||||||
evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
|
try:
|
||||||
except Exception as e:
|
evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
|
except Exception as e:
|
||||||
return False
|
logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
evaluated_logic = all(evaluated_conditions[1:])
|
||||||
|
|
||||||
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
|
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
|
||||||
return evaluated_logic
|
return evaluated_logic
|
||||||
@@ -312,13 +320,6 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
|
|
||||||
|
|
||||||
def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
|
def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
|
||||||
# Double check again if the notification has already been sent
|
|
||||||
if stream_data and \
|
|
||||||
any(d['notifier_id'] == notifier_id and d['notify_action'] == notify_action
|
|
||||||
for d in get_notify_state(session=stream_data)):
|
|
||||||
# Return if the notification has already been sent
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(u"Tautulli NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id)
|
logger.info(u"Tautulli NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id)
|
||||||
|
|
||||||
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||||
@@ -326,7 +327,7 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
|||||||
if not notifier_config:
|
if not notifier_config:
|
||||||
return
|
return
|
||||||
|
|
||||||
if notify_action == 'test':
|
if notify_action in ('test', 'api'):
|
||||||
subject = kwargs.pop('subject', 'Tautulli')
|
subject = kwargs.pop('subject', 'Tautulli')
|
||||||
body = kwargs.pop('body', 'Test Notification')
|
body = kwargs.pop('body', 'Test Notification')
|
||||||
script_args = kwargs.pop('script_args', [])
|
script_args = kwargs.pop('script_args', [])
|
||||||
@@ -344,8 +345,8 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
|||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
notification_id = set_notify_state(session=stream_data or timeline_data,
|
notification_id = set_notify_state(session=stream_data or timeline_data,
|
||||||
notify_action=notify_action,
|
|
||||||
notifier=notifier_config,
|
notifier=notifier_config,
|
||||||
|
notify_action=notify_action,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body,
|
body=body,
|
||||||
script_args=script_args)
|
script_args=script_args)
|
||||||
@@ -384,9 +385,31 @@ def get_notify_state(session):
|
|||||||
return notify_states
|
return notify_states
|
||||||
|
|
||||||
|
|
||||||
def set_notify_state(notify_action, notifier, subject, body, script_args, session=None):
|
def get_notify_state_enabled(session, notify_action, notified=True):
|
||||||
|
if notified:
|
||||||
|
timestamp_where = 'AND timestamp IS NOT NULL'
|
||||||
|
else:
|
||||||
|
timestamp_where = 'AND timestamp IS NULL'
|
||||||
|
|
||||||
if notify_action and notifier:
|
monitor_db = database.MonitorDatabase()
|
||||||
|
result = monitor_db.select('SELECT id AS notifier_id, timestamp '
|
||||||
|
'FROM notifiers '
|
||||||
|
'LEFT OUTER JOIN ('
|
||||||
|
'SELECT timestamp, notifier_id '
|
||||||
|
'FROM notify_log '
|
||||||
|
'WHERE session_key = ? '
|
||||||
|
'AND rating_key = ? '
|
||||||
|
'AND user_id = ? '
|
||||||
|
'AND notify_action = ?) AS t ON notifiers.id = t.notifier_id '
|
||||||
|
'WHERE %s = 1 %s' % (notify_action, timestamp_where),
|
||||||
|
args=[session['session_key'], session['rating_key'], session['user_id'], notify_action])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
||||||
|
|
||||||
|
if notifier and notify_action:
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
session = session or {}
|
session = session or {}
|
||||||
@@ -429,20 +452,6 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
||||||
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
|
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
|
||||||
|
|
||||||
# Get the server name
|
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
|
||||||
|
|
||||||
# Get the server uptime
|
|
||||||
plex_tv = plextv.PlexTV()
|
|
||||||
server_times = plex_tv.get_server_times()
|
|
||||||
|
|
||||||
if server_times:
|
|
||||||
updated_at = server_times['updated_at']
|
|
||||||
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
|
|
||||||
else:
|
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
|
|
||||||
server_uptime = 'N/A'
|
|
||||||
|
|
||||||
# Get metadata for the item
|
# Get metadata for the item
|
||||||
if session:
|
if session:
|
||||||
rating_key = session['rating_key']
|
rating_key = session['rating_key']
|
||||||
@@ -451,7 +460,11 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
|
|
||||||
notify_params = defaultdict(str)
|
notify_params = defaultdict(str)
|
||||||
if session:
|
if session:
|
||||||
|
# Reload json from raw stream info
|
||||||
|
if session.get('raw_stream_info'):
|
||||||
|
session.update(json.loads(session['raw_stream_info']))
|
||||||
notify_params.update(session)
|
notify_params.update(session)
|
||||||
|
|
||||||
if timeline:
|
if timeline:
|
||||||
notify_params.update(timeline)
|
notify_params.update(timeline)
|
||||||
|
|
||||||
@@ -494,9 +507,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
user_stream_count = len(user_sessions)
|
user_stream_count = len(user_sessions)
|
||||||
|
|
||||||
# Generate a combined transcode decision value
|
# Generate a combined transcode decision value
|
||||||
if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode':
|
if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode':
|
||||||
transcode_decision = 'Transcode'
|
transcode_decision = 'Transcode'
|
||||||
elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy':
|
elif session.get('stream_video_decision', '') == 'copy' or session.get('stream_audio_decision', '') == 'copy':
|
||||||
transcode_decision = 'Direct Stream'
|
transcode_decision = 'Direct Stream'
|
||||||
else:
|
else:
|
||||||
transcode_decision = 'Direct Play'
|
transcode_decision = 'Direct Play'
|
||||||
@@ -513,10 +526,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
remaining_duration = duration - view_offset
|
remaining_duration = duration - view_offset
|
||||||
|
|
||||||
# Build Plex URL
|
# Build Plex URL
|
||||||
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fnotify_params%2F{rating_key}'.format(
|
if notify_params['media_type'] == 'track':
|
||||||
|
plex_web_rating_key = notify_params['parent_rating_key']
|
||||||
|
else:
|
||||||
|
plex_web_rating_key = notify_params['rating_key']
|
||||||
|
|
||||||
|
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
||||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
rating_key=rating_key)
|
rating_key=plex_web_rating_key)
|
||||||
|
|
||||||
# Get media IDs from guid and build URLs
|
# Get media IDs from guid and build URLs
|
||||||
if 'imdb://' in notify_params['guid']:
|
if 'imdb://' in notify_params['guid']:
|
||||||
@@ -525,12 +543,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
||||||
|
|
||||||
if 'thetvdb://' in notify_params['guid']:
|
if 'thetvdb://' in notify_params['guid']:
|
||||||
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdb://')[1].split('/')[0]
|
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
|
||||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
||||||
|
|
||||||
elif 'thetvdbdvdorder://' in notify_params['guid']:
|
elif 'thetvdbdvdorder://' in notify_params['guid']:
|
||||||
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0]
|
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0].split('?')[0]
|
||||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
||||||
|
|
||||||
@@ -541,7 +559,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'
|
||||||
|
|
||||||
elif notify_params['media_type'] in ('show', 'season', 'episode'):
|
elif notify_params['media_type'] in ('show', 'season', 'episode'):
|
||||||
notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('/')[0]
|
notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0]
|
||||||
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
|
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
|
||||||
|
|
||||||
@@ -561,7 +579,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoveidb_json['imdb_id']
|
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoveidb_json['imdb_id']
|
||||||
|
|
||||||
elif notify_params.get('thetvdb_id') or notify_params.get('imdb_id'):
|
elif notify_params.get('thetvdb_id') or notify_params.get('imdb_id'):
|
||||||
themoviedb_info = lookup_themoviedb_by_id(rating_key=rating_key,
|
if notify_params['media_type'] in ('episode', 'track'):
|
||||||
|
lookup_key = notify_params['grandparent_rating_key']
|
||||||
|
elif notify_params['media_type'] in ('season', 'album'):
|
||||||
|
lookup_key = notify_params['parent_rating_key']
|
||||||
|
else:
|
||||||
|
lookup_key = rating_key
|
||||||
|
|
||||||
|
themoviedb_info = lookup_themoviedb_by_id(rating_key=lookup_key,
|
||||||
thetvdb_id=notify_params.get('thetvdb_id'),
|
thetvdb_id=notify_params.get('thetvdb_id'),
|
||||||
imdb_id=notify_params.get('imdb_id'))
|
imdb_id=notify_params.get('imdb_id'))
|
||||||
notify_params.update(themoviedb_info)
|
notify_params.update(themoviedb_info)
|
||||||
@@ -569,7 +594,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
# Get TVmaze info (for tv shows only)
|
# Get TVmaze info (for tv shows only)
|
||||||
if plexpy.CONFIG.TVMAZE_LOOKUP:
|
if plexpy.CONFIG.TVMAZE_LOOKUP:
|
||||||
if notify_params['media_type'] in ('show', 'season', 'episode') and (notify_params.get('thetvdb_id') or notify_params.get('imdb_id')):
|
if notify_params['media_type'] in ('show', 'season', 'episode') and (notify_params.get('thetvdb_id') or notify_params.get('imdb_id')):
|
||||||
tvmaze_info = lookup_tvmaze_by_id(rating_key=rating_key,
|
if notify_params['media_type'] in ('episode', 'track'):
|
||||||
|
lookup_key = notify_params['grandparent_rating_key']
|
||||||
|
elif notify_params['media_type'] in ('season', 'album'):
|
||||||
|
lookup_key = notify_params['parent_rating_key']
|
||||||
|
else:
|
||||||
|
lookup_key = rating_key
|
||||||
|
|
||||||
|
tvmaze_info = lookup_tvmaze_by_id(rating_key=lookup_key,
|
||||||
thetvdb_id=notify_params.get('thetvdb_id'),
|
thetvdb_id=notify_params.get('thetvdb_id'),
|
||||||
imdb_id=notify_params.get('imdb_id'))
|
imdb_id=notify_params.get('imdb_id'))
|
||||||
notify_params.update(tvmaze_info)
|
notify_params.update(tvmaze_info)
|
||||||
@@ -608,13 +640,17 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
album_name = ''
|
album_name = ''
|
||||||
track_name = ''
|
track_name = ''
|
||||||
|
|
||||||
num, num00 = format_group_index([helpers.cast_to_int(d['media_index'])
|
child_num = [helpers.cast_to_int(
|
||||||
for d in child_metadata if d['parent_rating_key'] == rating_key])
|
d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
|
||||||
|
num, num00 = format_group_index(child_num)
|
||||||
season_num, season_num00 = num, num00
|
season_num, season_num00 = num, num00
|
||||||
|
|
||||||
episode_num, episode_num00 = '', ''
|
episode_num, episode_num00 = '', ''
|
||||||
track_num, track_num00 = '', ''
|
track_num, track_num00 = '', ''
|
||||||
|
|
||||||
|
child_count = len(child_num)
|
||||||
|
grandchild_count = ''
|
||||||
|
|
||||||
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
|
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
|
||||||
and notify_params['media_type'] in ('season', 'album')):
|
and notify_params['media_type'] in ('season', 'album')):
|
||||||
show_name = notify_params['parent_title']
|
show_name = notify_params['parent_title']
|
||||||
@@ -622,14 +658,19 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
artist_name = notify_params['parent_title']
|
artist_name = notify_params['parent_title']
|
||||||
album_name = notify_params['title']
|
album_name = notify_params['title']
|
||||||
track_name = ''
|
track_name = ''
|
||||||
|
|
||||||
season_num = str(notify_params['media_index']).zfill(1)
|
season_num = str(notify_params['media_index']).zfill(1)
|
||||||
season_num00 = str(notify_params['media_index']).zfill(2)
|
season_num00 = str(notify_params['media_index']).zfill(2)
|
||||||
|
|
||||||
num, num00 = format_group_index([helpers.cast_to_int(d['media_index'])
|
grandchild_num = [helpers.cast_to_int(
|
||||||
for d in child_metadata if d['parent_rating_key'] == rating_key])
|
d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
|
||||||
|
num, num00 = format_group_index(grandchild_num)
|
||||||
episode_num, episode_num00 = num, num00
|
episode_num, episode_num00 = num, num00
|
||||||
track_num, track_num00 = num, num00
|
track_num, track_num00 = num, num00
|
||||||
|
|
||||||
|
child_count = 1
|
||||||
|
grandchild_count = len(grandchild_num)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
show_name = notify_params['grandparent_title']
|
show_name = notify_params['grandparent_title']
|
||||||
episode_name = notify_params['title']
|
episode_name = notify_params['title']
|
||||||
@@ -642,18 +683,26 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
episode_num00 = str(notify_params['media_index']).zfill(2)
|
episode_num00 = str(notify_params['media_index']).zfill(2)
|
||||||
track_num = str(notify_params['media_index']).zfill(1)
|
track_num = str(notify_params['media_index']).zfill(1)
|
||||||
track_num00 = str(notify_params['media_index']).zfill(2)
|
track_num00 = str(notify_params['media_index']).zfill(2)
|
||||||
|
child_count = 1
|
||||||
|
grandchild_count = 1
|
||||||
|
|
||||||
available_params = {
|
available_params = {
|
||||||
# Global paramaters
|
# Global paramaters
|
||||||
'plexpy_version': common.VERSION_NUMBER,
|
'tautulli_version': common.RELEASE,
|
||||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
|
||||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||||
'server_name': server_name,
|
'tautulli_commit': plexpy.CURRENT_VERSION,
|
||||||
'server_uptime': server_uptime,
|
'server_name': plexpy.CONFIG.PMS_NAME,
|
||||||
'server_version': server_times.get('version', ''),
|
'server_ip': plexpy.CONFIG.PMS_IP,
|
||||||
|
'server_port': plexpy.CONFIG.PMS_PORT,
|
||||||
|
'server_url': plexpy.CONFIG.PMS_URL,
|
||||||
|
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
|
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
||||||
|
'server_version': plexpy.CONFIG.PMS_VERSION,
|
||||||
'action': notify_action.lstrip('on_'),
|
'action': notify_action.lstrip('on_'),
|
||||||
'datestamp': arrow.now().format(date_format),
|
'datestamp': arrow.now().format(date_format),
|
||||||
'timestamp': arrow.now().format(time_format),
|
'timestamp': arrow.now().format(time_format),
|
||||||
|
'unixtime': int(time.time()),
|
||||||
# Stream parameters
|
# Stream parameters
|
||||||
'streams': stream_count,
|
'streams': stream_count,
|
||||||
'user_streams': user_stream_count,
|
'user_streams': user_stream_count,
|
||||||
@@ -745,6 +794,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
'episode_num00': episode_num00,
|
'episode_num00': episode_num00,
|
||||||
'track_num': track_num,
|
'track_num': track_num,
|
||||||
'track_num00': track_num00,
|
'track_num00': track_num00,
|
||||||
|
'season_count': child_count,
|
||||||
|
'episode_count': grandchild_count,
|
||||||
|
'album_count': child_count,
|
||||||
|
'track_count': grandchild_count,
|
||||||
'year': notify_params['year'],
|
'year': notify_params['year'],
|
||||||
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
|
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
|
||||||
if notify_params['originally_available_at'] else '',
|
if notify_params['originally_available_at'] else '',
|
||||||
@@ -762,6 +815,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
'writers': ', '.join(notify_params['writers']),
|
'writers': ', '.join(notify_params['writers']),
|
||||||
'actors': ', '.join(notify_params['actors']),
|
'actors': ', '.join(notify_params['actors']),
|
||||||
'genres': ', '.join(notify_params['genres']),
|
'genres': ', '.join(notify_params['genres']),
|
||||||
|
'labels': ', '.join(notify_params['labels']),
|
||||||
|
'collections': ', '.join(notify_params['collections']),
|
||||||
'summary': notify_params['summary'],
|
'summary': notify_params['summary'],
|
||||||
'tagline': notify_params['tagline'],
|
'tagline': notify_params['tagline'],
|
||||||
'rating': notify_params['rating'],
|
'rating': notify_params['rating'],
|
||||||
@@ -830,40 +885,34 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||||||
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
||||||
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
||||||
|
|
||||||
# Get the server name
|
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
|
||||||
|
|
||||||
# Get the server uptime
|
|
||||||
plex_tv = plextv.PlexTV()
|
|
||||||
server_times = plex_tv.get_server_times()
|
|
||||||
|
|
||||||
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
|
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
|
||||||
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
|
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
|
||||||
|
|
||||||
if server_times:
|
|
||||||
updated_at = server_times['updated_at']
|
|
||||||
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
|
|
||||||
else:
|
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
|
|
||||||
server_uptime = 'N/A'
|
|
||||||
|
|
||||||
available_params = {
|
available_params = {
|
||||||
# Global paramaters
|
# Global paramaters
|
||||||
'plexpy_version': common.VERSION_NUMBER,
|
'tautulli_version': common.RELEASE,
|
||||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
|
||||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||||
'server_name': server_name,
|
'tautulli_commit': plexpy.CURRENT_VERSION,
|
||||||
'server_uptime': server_uptime,
|
'server_name': plexpy.CONFIG.PMS_NAME,
|
||||||
'server_version': server_times.get('version', ''),
|
'server_ip': plexpy.CONFIG.PMS_IP,
|
||||||
|
'server_port': plexpy.CONFIG.PMS_PORT,
|
||||||
|
'server_url': plexpy.CONFIG.PMS_URL,
|
||||||
|
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
||||||
|
'server_version': plexpy.CONFIG.PMS_VERSION,
|
||||||
|
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
'action': notify_action.lstrip('on_'),
|
'action': notify_action.lstrip('on_'),
|
||||||
'datestamp': arrow.now().format(date_format),
|
'datestamp': arrow.now().format(date_format),
|
||||||
'timestamp': arrow.now().format(time_format),
|
'timestamp': arrow.now().format(time_format),
|
||||||
|
'unixtime': int(time.time()),
|
||||||
# Plex Media Server update parameters
|
# Plex Media Server update parameters
|
||||||
'update_version': pms_download_info['version'],
|
'update_version': pms_download_info['version'],
|
||||||
'update_url': pms_download_info['download_url'],
|
'update_url': pms_download_info['download_url'],
|
||||||
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
|
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
|
||||||
if pms_download_info['release_date'] else '',
|
if pms_download_info['release_date'] else '',
|
||||||
'update_channel': 'Beta' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public',
|
'update_channel': 'Beta' if update_channel == 'beta' else 'Public',
|
||||||
'update_platform': pms_download_info['platform'],
|
'update_platform': pms_download_info['platform'],
|
||||||
'update_distro': pms_download_info['distro'],
|
'update_distro': pms_download_info['distro'],
|
||||||
'update_distro_build': pms_download_info['build'],
|
'update_distro_build': pms_download_info['build'],
|
||||||
@@ -872,12 +921,13 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||||||
'update_changelog_added': pms_download_info['changelog_added'],
|
'update_changelog_added': pms_download_info['changelog_added'],
|
||||||
'update_changelog_fixed': pms_download_info['changelog_fixed'],
|
'update_changelog_fixed': pms_download_info['changelog_fixed'],
|
||||||
# Tautulli update parameters
|
# Tautulli update parameters
|
||||||
'plexpy_update_version': plexpy_download_info['tag_name'],
|
'tautulli_update_version': plexpy_download_info['tag_name'],
|
||||||
'plexpy_update_tar': plexpy_download_info['tarball_url'],
|
'tautulli_update_release_url': plexpy_download_info['html_url'],
|
||||||
'plexpy_update_zip': plexpy_download_info['zipball_url'],
|
'tautulli_update_tar': plexpy_download_info['tarball_url'],
|
||||||
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''),
|
'tautulli_update_zip': plexpy_download_info['zipball_url'],
|
||||||
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''),
|
'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),
|
||||||
'plexpy_update_changelog': plexpy_download_info['body']
|
'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''),
|
||||||
|
'tautulli_update_changelog': plexpy_download_info['body']
|
||||||
}
|
}
|
||||||
|
|
||||||
return available_params
|
return available_params
|
||||||
@@ -937,7 +987,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
|||||||
try:
|
try:
|
||||||
script_args = [custom_formatter.format(unicode(arg), **parameters) for arg in subject.split()]
|
script_args = [custom_formatter.format(unicode(arg), **parameters) for arg in subject.split()]
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in script argument. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
|
||||||
script_args = []
|
script_args = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
|
||||||
@@ -948,7 +998,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
|||||||
try:
|
try:
|
||||||
subject = custom_formatter.format(unicode(subject), **parameters)
|
subject = custom_formatter.format(unicode(subject), **parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in notification subject. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
||||||
subject = unicode(default_subject).format(**parameters)
|
subject = unicode(default_subject).format(**parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
||||||
@@ -957,7 +1007,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
|||||||
try:
|
try:
|
||||||
body = custom_formatter.format(unicode(body), **parameters)
|
body = custom_formatter.format(unicode(body), **parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in notification body. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
|
||||||
body = unicode(default_body).format(**parameters)
|
body = unicode(default_body).format(**parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
||||||
@@ -977,8 +1027,8 @@ def strip_tag(data, agent_id=None):
|
|||||||
'font': ['color']}
|
'font': ['color']}
|
||||||
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
||||||
|
|
||||||
elif agent_id == 10:
|
elif agent_id in (10, 14, 20):
|
||||||
# Don't remove tags for email
|
# Don't remove tags for Email, Slack, and Discord
|
||||||
return data
|
return data
|
||||||
|
|
||||||
elif agent_id == 13:
|
elif agent_id == 13:
|
||||||
@@ -1036,14 +1086,17 @@ def get_poster_info(poster_thumb, poster_key, poster_title):
|
|||||||
raise Exception(u'PMS image request failed')
|
raise Exception(u'PMS image request failed')
|
||||||
|
|
||||||
# Upload poster_thumb to Imgur and get link
|
# Upload poster_thumb to Imgur and get link
|
||||||
poster_url = helpers.uploadToImgur(poster_file, poster_title)
|
poster_url, delete_hash = helpers.upload_to_imgur(poster_file, poster_title)
|
||||||
|
|
||||||
if poster_url:
|
if poster_url:
|
||||||
# Create poster info
|
# Create poster info
|
||||||
poster_info = {'poster_title': poster_title, 'poster_url': poster_url}
|
poster_info = {'poster_title': poster_title, 'poster_url': poster_url}
|
||||||
|
|
||||||
# Save the poster url in the database
|
# Save the poster url in the database
|
||||||
data_factory.set_poster_url(rating_key=poster_key, poster_title=poster_title, poster_url=poster_url)
|
data_factory.set_poster_url(rating_key=poster_key,
|
||||||
|
poster_title=poster_title,
|
||||||
|
poster_url=poster_url,
|
||||||
|
delete_hash=delete_hash)
|
||||||
|
|
||||||
# Delete the cached poster
|
# Delete the cached poster
|
||||||
os.remove(poster_file)
|
os.remove(poster_file)
|
||||||
@@ -1193,6 +1246,17 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
|
|||||||
|
|
||||||
if response and not err_msg:
|
if response and not err_msg:
|
||||||
themoviedb_json = response.json()
|
themoviedb_json = response.json()
|
||||||
|
themoviedb_id = themoviedb_json['id']
|
||||||
|
themoviedb_url = 'https://www.themoviedb.org/{}/{}'.format(media_type, themoviedb_id)
|
||||||
|
|
||||||
|
keys = {'themoviedb_id': themoviedb_id}
|
||||||
|
themoviedb_info = {'rating_key': rating_key,
|
||||||
|
'imdb_id': themoviedb_json.get('imdb_id'),
|
||||||
|
'themoviedb_url': themoviedb_url,
|
||||||
|
'themoviedb_json': json.dumps(themoviedb_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if err_msg:
|
if err_msg:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
142
plexpy/plextv.py
142
plexpy/plextv.py
@@ -29,7 +29,7 @@ import pmsconnect
|
|||||||
import session
|
import session
|
||||||
|
|
||||||
|
|
||||||
def get_server_resources(return_presence=False):
|
def get_server_resources(return_presence=False, return_server=False, **kwargs):
|
||||||
if not return_presence:
|
if not return_presence:
|
||||||
logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
|
logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
|
||||||
|
|
||||||
@@ -42,9 +42,15 @@ def get_server_resources(return_presence=False):
|
|||||||
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
|
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
|
||||||
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
|
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
|
||||||
'pms_url': plexpy.CONFIG.PMS_URL,
|
'pms_url': plexpy.CONFIG.PMS_URL,
|
||||||
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL
|
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL,
|
||||||
|
'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
server.update(kwargs)
|
||||||
|
for k in ['pms_ssl', 'pms_is_remote', 'pms_is_cloud', 'pms_url_manual']:
|
||||||
|
server[k] = int(server[k])
|
||||||
|
|
||||||
if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
|
if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
|
||||||
scheme = 'https'
|
scheme = 'https'
|
||||||
else:
|
else:
|
||||||
@@ -55,7 +61,7 @@ def get_server_resources(return_presence=False):
|
|||||||
port=server['pms_port'])
|
port=server['pms_port'])
|
||||||
|
|
||||||
plex_tv = PlexTV()
|
plex_tv = PlexTV()
|
||||||
result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
result = plex_tv.get_server_connections(pms_identifier=server['pms_identifier'],
|
||||||
pms_ip=server['pms_ip'],
|
pms_ip=server['pms_ip'],
|
||||||
pms_port=server['pms_port'],
|
pms_port=server['pms_port'],
|
||||||
include_https=server['pms_ssl'])
|
include_https=server['pms_ssl'])
|
||||||
@@ -103,6 +109,9 @@ def get_server_resources(return_presence=False):
|
|||||||
server['pms_url'] = fallback_url
|
server['pms_url'] = fallback_url
|
||||||
logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
|
logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
|
||||||
|
|
||||||
|
if return_server:
|
||||||
|
return server
|
||||||
|
|
||||||
plexpy.CONFIG.process_kwargs(server)
|
plexpy.CONFIG.process_kwargs(server)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
@@ -144,14 +153,7 @@ class PlexTV(object):
|
|||||||
uri = '/users/sign_in.xml'
|
uri = '/users/sign_in.xml'
|
||||||
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
||||||
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
||||||
'X-Plex-Device-Name': 'Tautulli',
|
'Authorization': 'Basic %s' % base64string}
|
||||||
'X-Plex-Product': 'Tautulli',
|
|
||||||
'X-Plex-Version': plexpy.common.VERSION_NUMBER,
|
|
||||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
|
||||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
|
||||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
|
||||||
'Authorization': 'Basic %s' % base64string
|
|
||||||
}
|
|
||||||
|
|
||||||
request = self.request_handler.make_request(uri=uri,
|
request = self.request_handler.make_request(uri=uri,
|
||||||
request_type='POST',
|
request_type='POST',
|
||||||
@@ -318,6 +320,14 @@ class PlexTV(object):
|
|||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
def cloud_server_status(self, output_format=''):
|
||||||
|
uri = '/api/v2/cloud_server'
|
||||||
|
request = self.request_handler.make_request(uri=uri,
|
||||||
|
request_type='GET',
|
||||||
|
output_format=output_format)
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
||||||
def get_full_users_list(self):
|
def get_full_users_list(self):
|
||||||
friends_list = self.get_plextv_friends(output_format='xml')
|
friends_list = self.get_plextv_friends(output_format='xml')
|
||||||
own_account = self.get_plextv_user_details(output_format='xml')
|
own_account = self.get_plextv_user_details(output_format='xml')
|
||||||
@@ -331,18 +341,19 @@ class PlexTV(object):
|
|||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
|
own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
|
||||||
"username": helpers.get_xml_attr(a, 'username'),
|
"username": helpers.get_xml_attr(a, 'username'),
|
||||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||||
"email": helpers.get_xml_attr(a, 'email'),
|
"email": helpers.get_xml_attr(a, 'email'),
|
||||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||||
"is_allow_sync": None,
|
"is_admin": 1,
|
||||||
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
"is_allow_sync": None,
|
||||||
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
||||||
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
||||||
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
||||||
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
||||||
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
||||||
}
|
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
||||||
|
}
|
||||||
|
|
||||||
users_list.append(own_details)
|
users_list.append(own_details)
|
||||||
|
|
||||||
@@ -354,18 +365,19 @@ class PlexTV(object):
|
|||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
friend = {"user_id": helpers.get_xml_attr(a, 'id'),
|
friend = {"user_id": helpers.get_xml_attr(a, 'id'),
|
||||||
"username": helpers.get_xml_attr(a, 'title'),
|
"username": helpers.get_xml_attr(a, 'title'),
|
||||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||||
"email": helpers.get_xml_attr(a, 'email'),
|
"email": helpers.get_xml_attr(a, 'email'),
|
||||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
"is_admin": 0,
|
||||||
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
|
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||||
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
|
||||||
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
||||||
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
||||||
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
||||||
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
||||||
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
||||||
}
|
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
||||||
|
}
|
||||||
|
|
||||||
users_list.append(friend)
|
users_list.append(friend)
|
||||||
|
|
||||||
@@ -374,9 +386,19 @@ class PlexTV(object):
|
|||||||
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
|
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
|
||||||
rating_key_filter=None, sync_id_filter=None):
|
rating_key_filter=None, sync_id_filter=None):
|
||||||
|
|
||||||
if machine_id is None:
|
if not machine_id:
|
||||||
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||||
|
|
||||||
|
if isinstance(rating_key_filter, list):
|
||||||
|
rating_key_filter = [str(k) for k in rating_key_filter]
|
||||||
|
elif rating_key_filter:
|
||||||
|
rating_key_filter = [str(rating_key_filter)]
|
||||||
|
|
||||||
|
if isinstance(user_id_filter, list):
|
||||||
|
user_id_filter = [str(k) for k in user_id_filter]
|
||||||
|
elif user_id_filter:
|
||||||
|
user_id_filter = [str(user_id_filter)]
|
||||||
|
|
||||||
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
|
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
|
||||||
user_data = users.Users()
|
user_data = users.Users()
|
||||||
|
|
||||||
@@ -416,7 +438,7 @@ class PlexTV(object):
|
|||||||
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
|
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
|
||||||
|
|
||||||
# Filter by user_id
|
# Filter by user_id
|
||||||
if user_id_filter and str(user_id_filter) != device_user_id:
|
if user_id_filter and device_user_id not in user_id_filter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for synced in a.getElementsByTagName('SyncItems'):
|
for synced in a.getElementsByTagName('SyncItems'):
|
||||||
@@ -430,7 +452,7 @@ class PlexTV(object):
|
|||||||
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
|
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
|
||||||
|
|
||||||
# Filter by rating_key
|
# Filter by rating_key
|
||||||
if rating_key_filter and str(rating_key_filter) != rating_key:
|
if rating_key_filter and rating_key not in rating_key_filter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sync_id = helpers.get_xml_attr(item, 'id')
|
sync_id = helpers.get_xml_attr(item, 'id')
|
||||||
@@ -459,12 +481,13 @@ class PlexTV(object):
|
|||||||
status_item_downloaded_count, status_item_count)
|
status_item_downloaded_count, status_item_count)
|
||||||
|
|
||||||
for settings in item.getElementsByTagName('MediaSettings'):
|
for settings in item.getElementsByTagName('MediaSettings'):
|
||||||
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
|
settings_video_bitrate = helpers.get_xml_attr(settings, 'maxVideoBitrate')
|
||||||
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
|
|
||||||
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
|
||||||
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
|
||||||
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
|
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
|
||||||
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
|
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
|
||||||
|
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
|
||||||
|
settings_audio_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
|
||||||
|
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
||||||
|
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
||||||
|
|
||||||
sync_details = {"device_name": helpers.sanitize(device_name),
|
sync_details = {"device_name": helpers.sanitize(device_name),
|
||||||
"platform": helpers.sanitize(device_platform),
|
"platform": helpers.sanitize(device_platform),
|
||||||
@@ -481,7 +504,8 @@ class PlexTV(object):
|
|||||||
"item_complete_count": status_item_complete_count,
|
"item_complete_count": status_item_complete_count,
|
||||||
"item_downloaded_count": status_item_downloaded_count,
|
"item_downloaded_count": status_item_downloaded_count,
|
||||||
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
||||||
"music_bitrate": settings_music_bitrate,
|
"video_bitrate": settings_video_bitrate,
|
||||||
|
"audio_bitrate": settings_audio_bitrate,
|
||||||
"photo_quality": settings_photo_quality,
|
"photo_quality": settings_photo_quality,
|
||||||
"video_quality": settings_video_quality,
|
"video_quality": settings_video_quality,
|
||||||
"total_size": status_total_size,
|
"total_size": status_total_size,
|
||||||
@@ -630,8 +654,10 @@ class PlexTV(object):
|
|||||||
'label': helpers.get_xml_attr(d, 'name'),
|
'label': helpers.get_xml_attr(d, 'name'),
|
||||||
'ip': helpers.get_xml_attr(c, 'address'),
|
'ip': helpers.get_xml_attr(c, 'address'),
|
||||||
'port': helpers.get_xml_attr(c, 'port'),
|
'port': helpers.get_xml_attr(c, 'port'),
|
||||||
|
'uri': helpers.get_xml_attr(c, 'uri'),
|
||||||
'local': helpers.get_xml_attr(c, 'local'),
|
'local': helpers.get_xml_attr(c, 'local'),
|
||||||
'value': helpers.get_xml_attr(c, 'address')
|
'value': helpers.get_xml_attr(c, 'address'),
|
||||||
|
'is_cloud': is_cloud
|
||||||
}
|
}
|
||||||
clean_servers.append(server)
|
clean_servers.append(server)
|
||||||
|
|
||||||
@@ -639,10 +665,14 @@ class PlexTV(object):
|
|||||||
|
|
||||||
def get_plex_downloads(self):
|
def get_plex_downloads(self):
|
||||||
logger.debug(u"Tautulli PlexTV :: Retrieving current server version.")
|
logger.debug(u"Tautulli PlexTV :: Retrieving current server version.")
|
||||||
pmsconnect.PmsConnect().set_server_version()
|
|
||||||
|
|
||||||
logger.debug(u"Tautulli PlexTV :: Plex update channel is %s." % plexpy.CONFIG.PMS_UPDATE_CHANNEL)
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
plex_downloads = self.get_plextv_downloads(plexpass=(plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass'))
|
pms_connect.set_server_version()
|
||||||
|
|
||||||
|
update_channel = pms_connect.get_server_update_channel()
|
||||||
|
|
||||||
|
logger.debug(u"Tautulli PlexTV :: Plex update channel is %s." % update_channel)
|
||||||
|
plex_downloads = self.get_plextv_downloads(plexpass=(update_channel == 'beta'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
available_downloads = json.loads(plex_downloads)
|
available_downloads = json.loads(plex_downloads)
|
||||||
@@ -735,3 +765,21 @@ class PlexTV(object):
|
|||||||
devices_list.append(device)
|
devices_list.append(device)
|
||||||
|
|
||||||
return devices_list
|
return devices_list
|
||||||
|
|
||||||
|
def get_cloud_server_status(self):
|
||||||
|
cloud_status = self.cloud_server_status(output_format='xml')
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_info = cloud_status.getElementsByTagName('info')
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_cloud_server_status: %s." % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for info in status_info:
|
||||||
|
servers = info.getElementsByTagName('server')
|
||||||
|
for s in servers:
|
||||||
|
if helpers.get_xml_attr(s, 'address') == plexpy.CONFIG.PMS_IP:
|
||||||
|
if helpers.get_xml_attr(info, 'running') == '1':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class PmsConnect(object):
|
|||||||
self.url = plexpy.CONFIG.PMS_URL
|
self.url = plexpy.CONFIG.PMS_URL
|
||||||
elif not self.url:
|
elif not self.url:
|
||||||
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
||||||
port=plexpy.CONFIG.PMS_PORT)
|
port=plexpy.CONFIG.PMS_PORT)
|
||||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
||||||
|
|
||||||
if not self.token:
|
if not self.token:
|
||||||
@@ -533,17 +533,22 @@ class PmsConnect(object):
|
|||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
||||||
if cache_key:
|
if cache_key:
|
||||||
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
in_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||||
|
in_file_path = os.path.join(in_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
|
|
||||||
|
if not os.path.exists(in_file_folder):
|
||||||
|
os.mkdir(in_file_folder)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(in_file_path, 'r') as inFile:
|
with open(in_file_path, 'r') as inFile:
|
||||||
metadata = json.load(inFile)
|
metadata = json.load(inFile)
|
||||||
except IOError as e:
|
except (IOError, ValueError) as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
_cache_time = metadata.pop('_cache_time', 0)
|
_cache_time = metadata.pop('_cache_time', 0)
|
||||||
# Return cached metadata if less than 30 minutes ago
|
# Return cached metadata if less than METADATA_CACHE_SECONDS ago
|
||||||
if int(time.time()) - _cache_time <= 1800:
|
if int(time.time()) - _cache_time <= plexpy.CONFIG.METADATA_CACHE_SECONDS:
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if rating_key:
|
if rating_key:
|
||||||
@@ -559,27 +564,32 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
if a.getAttribute('size'):
|
if a.getAttribute('size'):
|
||||||
if a.getAttribute('size') != '1':
|
if a.getAttribute('size') == '0':
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if a.getElementsByTagName('Directory'):
|
if a.getElementsByTagName('Directory'):
|
||||||
metadata_main = a.getElementsByTagName('Directory')[0]
|
metadata_main_list = a.getElementsByTagName('Directory')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
if metadata_type == 'photo':
|
|
||||||
metadata_type = 'photo_album'
|
|
||||||
elif a.getElementsByTagName('Video'):
|
elif a.getElementsByTagName('Video'):
|
||||||
metadata_main = a.getElementsByTagName('Video')[0]
|
metadata_main_list = a.getElementsByTagName('Video')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
elif a.getElementsByTagName('Track'):
|
elif a.getElementsByTagName('Track'):
|
||||||
metadata_main = a.getElementsByTagName('Track')[0]
|
metadata_main_list = a.getElementsByTagName('Track')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
elif a.getElementsByTagName('Photo'):
|
elif a.getElementsByTagName('Photo'):
|
||||||
metadata_main = a.getElementsByTagName('Photo')[0]
|
metadata_main_list = a.getElementsByTagName('Photo')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
else:
|
else:
|
||||||
logger.debug(u"Tautulli Pmsconnect :: Metadata failed")
|
logger.debug(u"Tautulli Pmsconnect :: Metadata failed")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
if sync_id and len(metadata_main_list) > 1:
|
||||||
|
for metadata_main in metadata_main_list:
|
||||||
|
if helpers.get_xml_attr(metadata_main, 'ratingKey') == rating_key:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
metadata_main = metadata_main_list[0]
|
||||||
|
|
||||||
|
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
||||||
|
if metadata_main.nodeName == 'Directory' and metadata_type == 'photo':
|
||||||
|
metadata_type = 'photo_album'
|
||||||
|
|
||||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||||
library_name = helpers.get_xml_attr(a, 'librarySectionTitle')
|
library_name = helpers.get_xml_attr(a, 'librarySectionTitle')
|
||||||
|
|
||||||
@@ -588,6 +598,7 @@ class PmsConnect(object):
|
|||||||
actors = []
|
actors = []
|
||||||
genres = []
|
genres = []
|
||||||
labels = []
|
labels = []
|
||||||
|
collections = []
|
||||||
|
|
||||||
if metadata_main.getElementsByTagName('Director'):
|
if metadata_main.getElementsByTagName('Director'):
|
||||||
for director in metadata_main.getElementsByTagName('Director'):
|
for director in metadata_main.getElementsByTagName('Director'):
|
||||||
@@ -609,6 +620,10 @@ class PmsConnect(object):
|
|||||||
for label in metadata_main.getElementsByTagName('Label'):
|
for label in metadata_main.getElementsByTagName('Label'):
|
||||||
labels.append(helpers.get_xml_attr(label, 'tag'))
|
labels.append(helpers.get_xml_attr(label, 'tag'))
|
||||||
|
|
||||||
|
if metadata_main.getElementsByTagName('Collection'):
|
||||||
|
for collection in metadata_main.getElementsByTagName('Collection'):
|
||||||
|
collections.append(helpers.get_xml_attr(collection, 'tag'))
|
||||||
|
|
||||||
if metadata_type == 'movie':
|
if metadata_type == 'movie':
|
||||||
metadata = {'media_type': metadata_type,
|
metadata = {'media_type': metadata_type,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
@@ -646,10 +661,16 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'show':
|
elif metadata_type == 'show':
|
||||||
|
# Workaround for for duration sometimes reported in minutes for a show
|
||||||
|
duration = helpers.get_xml_attr(metadata_main, 'duration')
|
||||||
|
if duration.isdigit() and int(duration) < 1000:
|
||||||
|
duration = unicode(int(duration) * 60 * 1000)
|
||||||
|
|
||||||
metadata = {'media_type': metadata_type,
|
metadata = {'media_type': metadata_type,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
'library_name': library_name,
|
'library_name': library_name,
|
||||||
@@ -669,7 +690,7 @@ class PmsConnect(object):
|
|||||||
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
|
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
|
||||||
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
|
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
|
||||||
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
|
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
|
||||||
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
|
'duration': duration,
|
||||||
'year': helpers.get_xml_attr(metadata_main, 'year'),
|
'year': helpers.get_xml_attr(metadata_main, 'year'),
|
||||||
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
|
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
|
||||||
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
||||||
@@ -686,6 +707,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,6 +750,7 @@ class PmsConnect(object):
|
|||||||
'actors': show_details['actors'],
|
'actors': show_details['actors'],
|
||||||
'genres': show_details['genres'],
|
'genres': show_details['genres'],
|
||||||
'labels': show_details['labels'],
|
'labels': show_details['labels'],
|
||||||
|
'collections': show_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -771,6 +794,7 @@ class PmsConnect(object):
|
|||||||
'actors': show_details['actors'],
|
'actors': show_details['actors'],
|
||||||
'genres': show_details['genres'],
|
'genres': show_details['genres'],
|
||||||
'labels': show_details['labels'],
|
'labels': show_details['labels'],
|
||||||
|
'collections': show_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -812,6 +836,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,6 +879,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -897,6 +923,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': album_details['genres'],
|
'genres': album_details['genres'],
|
||||||
'labels': album_details['labels'],
|
'labels': album_details['labels'],
|
||||||
|
'collections': album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -938,6 +965,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,6 +1008,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': photo_album_details['genres'],
|
'genres': photo_album_details['genres'],
|
||||||
'labels': photo_album_details['labels'],
|
'labels': photo_album_details['labels'],
|
||||||
|
'collections': photo_album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -1025,6 +1054,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,6 +1095,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,14 +1186,19 @@ class PmsConnect(object):
|
|||||||
metadata['media_info'] = medias
|
metadata['media_info'] = medias
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
metadata['_cache_time'] = int(time.time())
|
|
||||||
|
|
||||||
if cache_key:
|
if cache_key:
|
||||||
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
metadata['_cache_time'] = int(time.time())
|
||||||
|
|
||||||
|
out_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||||
|
out_file_path = os.path.join(out_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
|
|
||||||
|
if not os.path.exists(out_file_folder):
|
||||||
|
os.mkdir(out_file_folder)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(out_file_path, 'w') as outFile:
|
with open(out_file_path, 'w') as outFile:
|
||||||
json.dump(metadata, outFile)
|
json.dump(metadata, outFile)
|
||||||
except IOError as e:
|
except (IOError, ValueError) as e:
|
||||||
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
|
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
|
||||||
% (cache_key, e))
|
% (cache_key, e))
|
||||||
|
|
||||||
@@ -1370,9 +1406,13 @@ class PmsConnect(object):
|
|||||||
else:
|
else:
|
||||||
session_details = {'session_id': '',
|
session_details = {'session_id': '',
|
||||||
'bandwidth': '',
|
'bandwidth': '',
|
||||||
'location': 'Unknown'
|
'location': 'wan' if player_details['local'] == '0' else 'lan'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if using Plex Relay
|
||||||
|
session_details['relay'] = int(session_details['location'] != 'lan'
|
||||||
|
and player_details['ip_address_public'] == '127.0.0.1')
|
||||||
|
|
||||||
# Get the transcode details
|
# Get the transcode details
|
||||||
if session.getElementsByTagName('TranscodeSession'):
|
if session.getElementsByTagName('TranscodeSession'):
|
||||||
transcode_info = session.getElementsByTagName('TranscodeSession')[0]
|
transcode_info = session.getElementsByTagName('TranscodeSession')[0]
|
||||||
@@ -1430,29 +1470,31 @@ class PmsConnect(object):
|
|||||||
transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS)
|
transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS)
|
||||||
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
|
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
|
||||||
|
|
||||||
# Generate a combined transcode decision value
|
|
||||||
if transcode_details['video_decision'] == 'transcode' or transcode_details['audio_decision'] == 'transcode':
|
|
||||||
transcode_decision = 'transcode'
|
|
||||||
elif transcode_details['video_decision'] == 'copy' or transcode_details['audio_decision'] == 'copy':
|
|
||||||
transcode_decision = 'copy'
|
|
||||||
else:
|
|
||||||
transcode_decision = 'direct play'
|
|
||||||
|
|
||||||
# Determine if a synced version is being played
|
# Determine if a synced version is being played
|
||||||
sync_id = None
|
sync_id = None
|
||||||
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
|
if media_type not in ('photo', 'clip') \
|
||||||
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
|
and not session.getElementsByTagName('Session') \
|
||||||
|
and not session.getElementsByTagName('TranscodeSession') \
|
||||||
|
and helpers.get_xml_attr(session, 'ratingKey').isdigit():
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
|
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
|
||||||
|
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
|
||||||
|
|
||||||
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
|
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
|
||||||
rating_key_filter=rating_key)
|
rating_key_filter=[rating_key, parent_rating_key, grandparent_rating_key])
|
||||||
if synced_items:
|
if synced_items:
|
||||||
sync_id = synced_items[0]['sync_id']
|
synced_item_details = synced_items[0]
|
||||||
|
sync_id = synced_item_details['sync_id']
|
||||||
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
|
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
|
||||||
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
|
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
|
||||||
if synced_xml_head[0].getElementsByTagName('Track'):
|
if synced_xml_head[0].getElementsByTagName('Track'):
|
||||||
synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0]
|
synced_xml_items = synced_xml_head[0].getElementsByTagName('Track')
|
||||||
elif synced_xml_head[0].getElementsByTagName('Video'):
|
elif synced_xml_head[0].getElementsByTagName('Video'):
|
||||||
synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0]
|
synced_xml_items = synced_xml_head[0].getElementsByTagName('Video')
|
||||||
|
|
||||||
|
for synced_session_data in synced_xml_items:
|
||||||
|
if helpers.get_xml_attr(synced_session_data, 'ratingKey') == rating_key:
|
||||||
|
break
|
||||||
|
|
||||||
# Figure out which version is being played
|
# Figure out which version is being played
|
||||||
if sync_id:
|
if sync_id:
|
||||||
@@ -1539,6 +1581,14 @@ class PmsConnect(object):
|
|||||||
'stream_subtitle_decision': ''
|
'stream_subtitle_decision': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Generate a combined transcode decision value
|
||||||
|
if video_details['stream_video_decision'] == 'transcode' or audio_details['stream_audio_decision'] == 'transcode':
|
||||||
|
transcode_decision = 'transcode'
|
||||||
|
elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy':
|
||||||
|
transcode_decision = 'copy'
|
||||||
|
else:
|
||||||
|
transcode_decision = 'direct play'
|
||||||
|
|
||||||
# Get the bif thumbnail
|
# Get the bif thumbnail
|
||||||
indexes = helpers.get_xml_attr(stream_media_parts_info, 'indexes')
|
indexes = helpers.get_xml_attr(stream_media_parts_info, 'indexes')
|
||||||
view_offset = helpers.get_xml_attr(session, 'viewOffset')
|
view_offset = helpers.get_xml_attr(session, 'viewOffset')
|
||||||
@@ -1586,6 +1636,7 @@ class PmsConnect(object):
|
|||||||
channel_stream = 1
|
channel_stream = 1
|
||||||
|
|
||||||
clip_media = session.getElementsByTagName('Media')[0]
|
clip_media = session.getElementsByTagName('Media')[0]
|
||||||
|
clip_part = clip_media.getElementsByTagName('Part')[0]
|
||||||
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
|
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
|
||||||
metadata_details = {'media_type': media_type,
|
metadata_details = {'media_type': media_type,
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
@@ -1624,7 +1675,8 @@ class PmsConnect(object):
|
|||||||
'genres': [],
|
'genres': [],
|
||||||
'labels': [],
|
'labels': [],
|
||||||
'full_title': helpers.get_xml_attr(session, 'title'),
|
'full_title': helpers.get_xml_attr(session, 'title'),
|
||||||
'container': helpers.get_xml_attr(clip_media, 'container'),
|
'container': helpers.get_xml_attr(clip_media, 'container') \
|
||||||
|
or helpers.get_xml_attr(clip_part, 'container'),
|
||||||
'height': helpers.get_xml_attr(clip_media, 'height'),
|
'height': helpers.get_xml_attr(clip_media, 'height'),
|
||||||
'width': helpers.get_xml_attr(clip_media, 'width'),
|
'width': helpers.get_xml_attr(clip_media, 'width'),
|
||||||
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
|
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
|
||||||
@@ -1633,7 +1685,8 @@ class PmsConnect(object):
|
|||||||
'audio_channels': audio_channels,
|
'audio_channels': audio_channels,
|
||||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||||
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
||||||
'channel_title': helpers.get_xml_attr(session, 'sourceTitle')
|
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
|
||||||
|
'live': int(helpers.get_xml_attr(session, 'live') == '1')
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
channel_stream = 0
|
channel_stream = 0
|
||||||
@@ -1642,7 +1695,7 @@ class PmsConnect(object):
|
|||||||
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
|
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
|
||||||
|
|
||||||
if sync_id:
|
if sync_id:
|
||||||
metadata_details = self.get_metadata_details(sync_id=sync_id, cache_key=session_key)
|
metadata_details = self.get_metadata_details(rating_key=rating_key, sync_id=sync_id, cache_key=session_key)
|
||||||
else:
|
else:
|
||||||
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
|
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
|
||||||
|
|
||||||
@@ -1699,51 +1752,72 @@ class PmsConnect(object):
|
|||||||
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
|
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
|
||||||
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
||||||
|
|
||||||
|
# Overrides for live sessions
|
||||||
|
if metadata_details.get('live') and transcode_decision == 'transcode':
|
||||||
|
stream_details['stream_container_decision'] = 'transcode'
|
||||||
|
stream_details['stream_container'] = transcode_details['transcode_container']
|
||||||
|
|
||||||
|
video_details['stream_video_decision'] = transcode_details['video_decision']
|
||||||
|
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
|
||||||
|
stream_details['stream_video_resolution'] = metadata_details['video_resolution']
|
||||||
|
|
||||||
|
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
|
||||||
|
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
|
||||||
|
stream_details['stream_audio_channels'] = transcode_details['transcode_audio_channels']
|
||||||
|
stream_details['stream_audio_channel_layout'] = common.AUDIO_CHANNELS.get(
|
||||||
|
transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels'])
|
||||||
|
|
||||||
# Get the quality profile
|
# Get the quality profile
|
||||||
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
||||||
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
if sync_id:
|
||||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
quailtiy_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
|
||||||
quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate]
|
|
||||||
except ValueError:
|
|
||||||
quality_profile = 'Original'
|
quality_profile = 'Original'
|
||||||
|
|
||||||
if sync_id:
|
synced_item_bitrate = helpers.cast_to_int(synced_item_details['video_bitrate'])
|
||||||
try:
|
try:
|
||||||
synced_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if source_bitrate <= b)
|
synced_bitrate = max(b for b in common.VIDEO_QUALITY_PROFILES if b <= synced_item_bitrate)
|
||||||
synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate]
|
synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
synced_version_profile = 'Original'
|
synced_version_profile = 'Original'
|
||||||
else:
|
else:
|
||||||
synced_version_profile = ''
|
synced_version_profile = ''
|
||||||
|
|
||||||
|
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
||||||
|
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||||
|
try:
|
||||||
|
quailtiy_bitrate = min(
|
||||||
|
b for b in common.VIDEO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
||||||
|
quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate]
|
||||||
|
except ValueError:
|
||||||
|
quality_profile = 'Original'
|
||||||
|
|
||||||
if stream_details['optimized_version']:
|
if stream_details['optimized_version']:
|
||||||
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
||||||
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'], source_media_details['video_resolution']))
|
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'],
|
||||||
|
source_media_details['video_resolution']))
|
||||||
else:
|
else:
|
||||||
optimized_version_profile = ''
|
optimized_version_profile = ''
|
||||||
|
|
||||||
elif media_type == 'track' and 'stream_bitrate' in stream_details:
|
elif media_type == 'track' and 'stream_bitrate' in stream_details:
|
||||||
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
if sync_id:
|
||||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
|
||||||
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
|
|
||||||
except ValueError:
|
|
||||||
quality_profile = 'Original'
|
quality_profile = 'Original'
|
||||||
|
|
||||||
if sync_id:
|
synced_item_bitrate = helpers.cast_to_int(synced_item_details['audio_bitrate'])
|
||||||
try:
|
try:
|
||||||
synced_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if source_bitrate <= b)
|
synced_bitrate = max(b for b in common.AUDIO_QUALITY_PROFILES if b <= synced_item_bitrate)
|
||||||
synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate]
|
synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
synced_version_profile = 'Original'
|
synced_version_profile = 'Original'
|
||||||
else:
|
else:
|
||||||
synced_version_profile = ''
|
synced_version_profile = ''
|
||||||
|
|
||||||
|
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
||||||
|
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||||
|
try:
|
||||||
|
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
||||||
|
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
|
||||||
|
except ValueError:
|
||||||
|
quality_profile = 'Original'
|
||||||
|
|
||||||
optimized_version_profile = ''
|
optimized_version_profile = ''
|
||||||
|
|
||||||
elif media_type == 'photo':
|
elif media_type == 'photo':
|
||||||
@@ -2071,10 +2145,12 @@ class PmsConnect(object):
|
|||||||
sort_type = '&type=10'
|
sort_type = '&type=10'
|
||||||
elif section_type == 'photo':
|
elif section_type == 'photo':
|
||||||
sort_type = ''
|
sort_type = ''
|
||||||
elif section_type == 'photoAlbum':
|
elif section_type == 'photo_album':
|
||||||
sort_type = '&type=14'
|
sort_type = '&type=14'
|
||||||
elif section_type == 'picture':
|
elif section_type == 'picture':
|
||||||
sort_type = '&type=13'
|
sort_type = '&type=13&clusterZoomLevel=1'
|
||||||
|
elif section_type == 'clip':
|
||||||
|
sort_type = '&type=12&clusterZoomLevel=1'
|
||||||
else:
|
else:
|
||||||
sort_type = ''
|
sort_type = ''
|
||||||
|
|
||||||
@@ -2092,16 +2168,16 @@ class PmsConnect(object):
|
|||||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_library_children_details: %s." % e)
|
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_library_children_details: %s." % e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
childern_list = []
|
children_list = []
|
||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
if a.getAttribute('size'):
|
if a.getAttribute('size'):
|
||||||
if a.getAttribute('size') == '0':
|
if a.getAttribute('size') == '0':
|
||||||
logger.debug(u"Tautulli Pmsconnect :: No library data.")
|
logger.debug(u"Tautulli Pmsconnect :: No library data.")
|
||||||
childern_list = {'library_count': '0',
|
children_list = {'library_count': '0',
|
||||||
'childern_list': []
|
'children_list': []
|
||||||
}
|
}
|
||||||
return childern_list
|
return children_list
|
||||||
|
|
||||||
if rating_key:
|
if rating_key:
|
||||||
library_count = helpers.get_xml_attr(xml_head[0], 'size')
|
library_count = helpers.get_xml_attr(xml_head[0], 'size')
|
||||||
@@ -2122,8 +2198,12 @@ class PmsConnect(object):
|
|||||||
item_main += a.getElementsByTagName('Photo')
|
item_main += a.getElementsByTagName('Photo')
|
||||||
|
|
||||||
for item in item_main:
|
for item in item_main:
|
||||||
|
media_type = helpers.get_xml_attr(item, 'type')
|
||||||
|
if item.nodeName == 'Directory' and media_type == 'photo':
|
||||||
|
media_type = 'photo_album'
|
||||||
|
|
||||||
item_info = {'section_id': helpers.get_xml_attr(a, 'librarySectionID'),
|
item_info = {'section_id': helpers.get_xml_attr(a, 'librarySectionID'),
|
||||||
'media_type': helpers.get_xml_attr(item, 'type'),
|
'media_type': media_type,
|
||||||
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
|
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
|
||||||
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
|
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
|
||||||
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
|
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
|
||||||
@@ -2155,10 +2235,10 @@ class PmsConnect(object):
|
|||||||
}
|
}
|
||||||
item_info.update(media_info)
|
item_info.update(media_info)
|
||||||
|
|
||||||
childern_list.append(item_info)
|
children_list.append(item_info)
|
||||||
|
|
||||||
output = {'library_count': library_count,
|
output = {'library_count': library_count,
|
||||||
'childern_list': childern_list
|
'children_list': children_list
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
@@ -2213,12 +2293,12 @@ class PmsConnect(object):
|
|||||||
library_stats.update(child_stats)
|
library_stats.update(child_stats)
|
||||||
|
|
||||||
if section_type == 'photo':
|
if section_type == 'photo':
|
||||||
parent_list = self.get_library_children_details(section_id=section_id, section_type='photoAlbum', count='1')
|
parent_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
|
||||||
if parent_list:
|
if parent_list:
|
||||||
parent_stats = {'parent_count': parent_list['library_count']}
|
parent_stats = {'parent_count': parent_list['library_count']}
|
||||||
library_stats.update(parent_stats)
|
library_stats.update(parent_stats)
|
||||||
|
|
||||||
child_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
|
child_list = self.get_library_children_details(section_id=section_id, section_type='clip', count='1')
|
||||||
if child_list:
|
if child_list:
|
||||||
child_stats = {'child_count': child_list['library_count']}
|
child_stats = {'child_count': child_list['library_count']}
|
||||||
library_stats.update(child_stats)
|
library_stats.update(child_stats)
|
||||||
@@ -2533,4 +2613,15 @@ class PmsConnect(object):
|
|||||||
version = identity.get('version', plexpy.CONFIG.PMS_VERSION)
|
version = identity.get('version', plexpy.CONFIG.PMS_VERSION)
|
||||||
|
|
||||||
plexpy.CONFIG.__setattr__('PMS_VERSION', version)
|
plexpy.CONFIG.__setattr__('PMS_VERSION', version)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
|
def get_server_update_channel(self):
|
||||||
|
if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plex':
|
||||||
|
update_channel_value = self.get_server_pref('ButlerUpdateChannel')
|
||||||
|
|
||||||
|
if update_channel_value == '8':
|
||||||
|
return 'beta'
|
||||||
|
else:
|
||||||
|
return 'public'
|
||||||
|
|
||||||
|
return plexpy.CONFIG.PMS_UPDATE_CHANNEL
|
||||||
|
|||||||
@@ -23,30 +23,29 @@ def get_session_info():
|
|||||||
"""
|
"""
|
||||||
Returns the session info for the user session
|
Returns the session info for the user session
|
||||||
"""
|
"""
|
||||||
from plexpy.webauth import SESSION_KEY
|
|
||||||
|
|
||||||
_session = {'user_id': None,
|
_session = {'user_id': None,
|
||||||
'user': None,
|
'user': None,
|
||||||
'user_group': 'admin',
|
'user_group': 'admin',
|
||||||
'expiry': None}
|
'exp': None}
|
||||||
try:
|
|
||||||
return cherrypy.session.get(SESSION_KEY, _session)
|
if isinstance(cherrypy.request.login, dict):
|
||||||
except AttributeError as e:
|
return cherrypy.request.login
|
||||||
return _session
|
|
||||||
|
return _session
|
||||||
|
|
||||||
def get_session_user():
|
def get_session_user():
|
||||||
"""
|
"""
|
||||||
Returns the user_id for the current logged in session
|
Returns the user_id for the current logged in session
|
||||||
"""
|
"""
|
||||||
_session = get_session_info()
|
_session = get_session_info()
|
||||||
return _session['user'] if _session and _session['user'] else None
|
return _session['user'] if _session['user_group'] == 'guest' and _session['user'] else None
|
||||||
|
|
||||||
def get_session_user_id():
|
def get_session_user_id():
|
||||||
"""
|
"""
|
||||||
Returns the user_id for the current logged in session
|
Returns the user_id for the current logged in session
|
||||||
"""
|
"""
|
||||||
_session = get_session_info()
|
_session = get_session_info()
|
||||||
return str(_session['user_id']) if _session and _session['user_id'] else None
|
return str(_session['user_id']) if _session['user_group'] == 'guest' and _session['user_id'] else None
|
||||||
|
|
||||||
def get_session_shared_libraries():
|
def get_session_shared_libraries():
|
||||||
"""
|
"""
|
||||||
@@ -80,7 +79,7 @@ def get_session_library_filters_type(filters, media_type=None):
|
|||||||
filters = filters.get('filter_tv', ())
|
filters = filters.get('filter_tv', ())
|
||||||
elif media_type == 'artist' or media_type == 'album' or media_type == 'track':
|
elif media_type == 'artist' or media_type == 'album' or media_type == 'track':
|
||||||
filters = filters.get('filter_music', ())
|
filters = filters.get('filter_music', ())
|
||||||
elif media_type == 'photo' or media_type == 'photoAlbum' or media_type == 'picture':
|
elif media_type == 'photo' or media_type == 'photo_album' or media_type == 'picture' or media_type == 'clip':
|
||||||
filters = filters.get('filter_photos', ())
|
filters = filters.get('filter_photos', ())
|
||||||
else:
|
else:
|
||||||
filters = filters.get('filter_all', ())
|
filters = filters.get('filter_all', ())
|
||||||
@@ -192,11 +191,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
|||||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||||
'ip_address': 'N/A',
|
'ip_address': 'N/A',
|
||||||
'machine_id': '',
|
'machine_id': '',
|
||||||
'platform': 'Platform',
|
'player': 'Player'
|
||||||
'player': 'Player',
|
|
||||||
'quality_profile': 'Unknown',
|
|
||||||
'bandwidth': '',
|
|
||||||
'location': ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata_to_mask = {'media_index': '0',
|
metadata_to_mask = {'media_index': '0',
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ def refresh_users():
|
|||||||
new_value_dict = {"username": item['username'],
|
new_value_dict = {"username": item['username'],
|
||||||
"thumb": item['thumb'],
|
"thumb": item['thumb'],
|
||||||
"email": item['email'],
|
"email": item['email'],
|
||||||
|
"is_admin": item['is_admin'],
|
||||||
"is_home_user": item['is_home_user'],
|
"is_home_user": item['is_home_user'],
|
||||||
"is_allow_sync": item['is_allow_sync'],
|
"is_allow_sync": item['is_allow_sync'],
|
||||||
"is_restricted": item['is_restricted'],
|
"is_restricted": item['is_restricted'],
|
||||||
@@ -330,6 +331,7 @@ class Users(object):
|
|||||||
'friendly_name': 'Local',
|
'friendly_name': 'Local',
|
||||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||||
'email': '',
|
'email': '',
|
||||||
|
'is_admin': '',
|
||||||
'is_home_user': 0,
|
'is_home_user': 0,
|
||||||
'is_allow_sync': 0,
|
'is_allow_sync': 0,
|
||||||
'is_restricted': 0,
|
'is_restricted': 0,
|
||||||
@@ -349,21 +351,21 @@ class Users(object):
|
|||||||
try:
|
try:
|
||||||
if str(user_id).isdigit():
|
if str(user_id).isdigit():
|
||||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
'email, is_admin, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
||||||
'allow_guest, shared_libraries ' \
|
'allow_guest, shared_libraries ' \
|
||||||
'FROM users ' \
|
'FROM users ' \
|
||||||
'WHERE user_id = ? '
|
'WHERE user_id = ? '
|
||||||
result = monitor_db.select(query, args=[user_id])
|
result = monitor_db.select(query, args=[user_id])
|
||||||
elif user:
|
elif user:
|
||||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
'email, is_admin, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
||||||
'allow_guest, shared_libraries ' \
|
'allow_guest, shared_libraries ' \
|
||||||
'FROM users ' \
|
'FROM users ' \
|
||||||
'WHERE username = ? COLLATE NOCASE '
|
'WHERE username = ? COLLATE NOCASE '
|
||||||
result = monitor_db.select(query, args=[user])
|
result = monitor_db.select(query, args=[user])
|
||||||
elif email:
|
elif email:
|
||||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
'email, is_admin, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, deleted_user, ' \
|
||||||
'allow_guest, shared_libraries ' \
|
'allow_guest, shared_libraries ' \
|
||||||
'FROM users ' \
|
'FROM users ' \
|
||||||
'WHERE email = ? COLLATE NOCASE '
|
'WHERE email = ? COLLATE NOCASE '
|
||||||
@@ -398,6 +400,7 @@ class Users(object):
|
|||||||
'friendly_name': friendly_name,
|
'friendly_name': friendly_name,
|
||||||
'user_thumb': user_thumb,
|
'user_thumb': user_thumb,
|
||||||
'email': item['email'],
|
'email': item['email'],
|
||||||
|
'is_admin': item['is_admin'],
|
||||||
'is_home_user': item['is_home_user'],
|
'is_home_user': item['is_home_user'],
|
||||||
'is_allow_sync': item['is_allow_sync'],
|
'is_allow_sync': item['is_allow_sync'],
|
||||||
'is_restricted': item['is_restricted'],
|
'is_restricted': item['is_restricted'],
|
||||||
@@ -580,6 +583,27 @@ class Users(object):
|
|||||||
|
|
||||||
return recently_watched
|
return recently_watched
|
||||||
|
|
||||||
|
def get_users(self):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = 'SELECT user_id, username, friendly_name, email FROM users WHERE deleted_user = 0'
|
||||||
|
result = monitor_db.select(query=query)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Tautulli Users :: Unable to execute database query for get_users: %s." % e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
users = []
|
||||||
|
for item in result:
|
||||||
|
user = {'user_id': item['user_id'],
|
||||||
|
'username': item['username'],
|
||||||
|
'friendly_name': item['friendly_name'] or item['username'],
|
||||||
|
'email': item['email']
|
||||||
|
}
|
||||||
|
users.append(user)
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
def delete_all_history(self, user_id=None):
|
def delete_all_history(self, user_id=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "beta"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.0.10-beta"
|
PLEXPY_RELEASE_VERSION = "v2.0.23-beta"
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import subprocess
|
|||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
|
import common
|
||||||
import logger
|
import logger
|
||||||
import request
|
import request
|
||||||
import version
|
|
||||||
|
|
||||||
|
|
||||||
def runGit(args):
|
def runGit(args):
|
||||||
@@ -65,7 +65,7 @@ def runGit(args):
|
|||||||
|
|
||||||
def getVersion():
|
def getVersion():
|
||||||
|
|
||||||
if version.PLEXPY_BRANCH.startswith('win32build'):
|
if common.BRANCH.startswith('win32build'):
|
||||||
plexpy.INSTALL_TYPE = 'win'
|
plexpy.INSTALL_TYPE = 'win'
|
||||||
|
|
||||||
# Don't have a way to update exe yet, but don't want to set VERSION to None
|
# Don't have a way to update exe yet, but don't want to set VERSION to None
|
||||||
@@ -120,15 +120,15 @@ def getVersion():
|
|||||||
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
||||||
|
|
||||||
if not os.path.isfile(version_file):
|
if not os.path.isfile(version_file):
|
||||||
return None, 'origin', 'master'
|
return None, 'origin', common.BRANCH
|
||||||
|
|
||||||
with open(version_file, 'r') as f:
|
with open(version_file, 'r') as f:
|
||||||
current_version = f.read().strip(' \n\r')
|
current_version = f.read().strip(' \n\r')
|
||||||
|
|
||||||
if current_version:
|
if current_version:
|
||||||
return current_version, plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH
|
return current_version, 'origin', common.BRANCH
|
||||||
else:
|
else:
|
||||||
return None, 'origin', 'master'
|
return None, 'origin', common.BRANCH
|
||||||
|
|
||||||
|
|
||||||
def checkGithub(auto_update=False):
|
def checkGithub(auto_update=False):
|
||||||
@@ -136,7 +136,9 @@ def checkGithub(auto_update=False):
|
|||||||
|
|
||||||
# Get the latest version available from github
|
# Get the latest version available from github
|
||||||
logger.info('Retrieving latest version information from GitHub')
|
logger.info('Retrieving latest version information from GitHub')
|
||||||
url = 'https://api.github.com/repos/%s/plexpy/commits/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH)
|
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
|
||||||
|
plexpy.CONFIG.GIT_REPO,
|
||||||
|
plexpy.CONFIG.GIT_BRANCH)
|
||||||
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
|
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
|
||||||
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
|
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
|
||||||
|
|
||||||
@@ -157,7 +159,10 @@ def checkGithub(auto_update=False):
|
|||||||
return plexpy.LATEST_VERSION
|
return plexpy.LATEST_VERSION
|
||||||
|
|
||||||
logger.info('Comparing currently installed version with latest GitHub version')
|
logger.info('Comparing currently installed version with latest GitHub version')
|
||||||
url = 'https://api.github.com/repos/%s/plexpy/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.LATEST_VERSION, plexpy.CURRENT_VERSION)
|
url = 'https://api.github.com/repos/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER,
|
||||||
|
plexpy.CONFIG.GIT_REPO,
|
||||||
|
plexpy.LATEST_VERSION,
|
||||||
|
plexpy.CURRENT_VERSION)
|
||||||
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
|
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
|
||||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||||
|
|
||||||
@@ -175,7 +180,7 @@ def checkGithub(auto_update=False):
|
|||||||
if plexpy.COMMITS_BEHIND > 0:
|
if plexpy.COMMITS_BEHIND > 0:
|
||||||
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
|
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
|
||||||
|
|
||||||
url = 'https://api.github.com/repos/%s/plexpy/releases' % plexpy.CONFIG.GIT_USER
|
url = 'https://api.github.com/repos/%s/%s/releases' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)
|
||||||
releases = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
|
releases = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
|
||||||
|
|
||||||
if releases is None:
|
if releases is None:
|
||||||
@@ -185,12 +190,14 @@ def checkGithub(auto_update=False):
|
|||||||
if plexpy.CONFIG.GIT_BRANCH == 'master':
|
if plexpy.CONFIG.GIT_BRANCH == 'master':
|
||||||
release = next((r for r in releases if not r['prerelease']), releases[0])
|
release = next((r for r in releases if not r['prerelease']), releases[0])
|
||||||
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
|
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
|
||||||
release = next((r for r in releases if r['prerelease'] and '-beta' in r['tag_name']), releases[0])
|
release = next((r for r in releases if not r['tag_name'].endswith('-nightly')), releases[0])
|
||||||
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
|
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
|
||||||
release = next((r for r in releases if r['prerelease'] and '-nightly' in r['tag_name']), releases[0])
|
release = next((r for r in releases), releases[0])
|
||||||
else:
|
else:
|
||||||
release = releases[0]
|
release = releases[0]
|
||||||
|
|
||||||
|
plexpy.LATEST_RELEASE = release['tag_name']
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', 'plexpy_download_info': release,
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', 'plexpy_download_info': release,
|
||||||
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
||||||
|
|
||||||
@@ -285,8 +292,8 @@ def update():
|
|||||||
|
|
||||||
def checkout_git_branch():
|
def checkout_git_branch():
|
||||||
if plexpy.INSTALL_TYPE == 'git':
|
if plexpy.INSTALL_TYPE == 'git':
|
||||||
output, err = runGit('fetch ' + plexpy.CONFIG.GIT_REMOTE)
|
output, err = runGit('fetch %s' % plexpy.CONFIG.GIT_REMOTE)
|
||||||
output, err = runGit('checkout ' + plexpy.CONFIG.GIT_BRANCH)
|
output, err = runGit('checkout %s' % plexpy.CONFIG.GIT_BRANCH)
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
logger.error('Unable to change git branch.')
|
logger.error('Unable to change git branch.')
|
||||||
@@ -297,15 +304,17 @@ def checkout_git_branch():
|
|||||||
logger.error('Unable to checkout from git: ' + line)
|
logger.error('Unable to checkout from git: ' + line)
|
||||||
logger.info('Output: ' + str(output))
|
logger.info('Output: ' + str(output))
|
||||||
|
|
||||||
|
output, err = runGit('pull %s %s' % (plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH))
|
||||||
|
|
||||||
def read_changelog(latest_only=False):
|
|
||||||
|
def read_changelog(latest_only=False, since_prev_release=False):
|
||||||
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
||||||
|
|
||||||
if not os.path.isfile(changelog_file):
|
if not os.path.isfile(changelog_file):
|
||||||
return '<h4>Missing changelog file</h4>'
|
return '<h4>Missing changelog file</h4>'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output = ''
|
output = ['']
|
||||||
prev_level = 0
|
prev_level = 0
|
||||||
|
|
||||||
latest_version_found = False
|
latest_version_found = False
|
||||||
@@ -329,27 +338,34 @@ def read_changelog(latest_only=False):
|
|||||||
break
|
break
|
||||||
elif latest_only:
|
elif latest_only:
|
||||||
latest_version_found = True
|
latest_version_found = True
|
||||||
|
# Add a space to the end of the release to match tags
|
||||||
|
elif since_prev_release and str(plexpy.PREV_RELEASE) + ' ' in header_text:
|
||||||
|
break
|
||||||
|
|
||||||
output += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
||||||
|
|
||||||
elif line_list_match:
|
elif line_list_match:
|
||||||
line_level = len(line_list_match.group(1)) / 2
|
line_level = len(line_list_match.group(1)) / 2
|
||||||
line_text = line_list_match.group(2)
|
line_text = line_list_match.group(2)
|
||||||
|
|
||||||
if line_level > prev_level:
|
if line_level > prev_level:
|
||||||
output += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
|
output[-1] += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
|
||||||
elif line_level < prev_level:
|
elif line_level < prev_level:
|
||||||
output += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
|
output[-1] += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
|
||||||
else:
|
else:
|
||||||
output += '<li>' + line_text + '</li>'
|
output[-1] += '<li>' + line_text + '</li>'
|
||||||
|
|
||||||
prev_level = line_level
|
prev_level = line_level
|
||||||
|
|
||||||
elif line.strip() == '' and prev_level:
|
elif line.strip() == '' and prev_level:
|
||||||
output += '</ul>' * (prev_level)
|
output[-1] += '</ul>' * (prev_level)
|
||||||
|
output.append('')
|
||||||
prev_level = 0
|
prev_level = 0
|
||||||
|
|
||||||
return output
|
if since_prev_release:
|
||||||
|
output.reverse()
|
||||||
|
|
||||||
|
return ''.join(output)
|
||||||
|
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)
|
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)
|
||||||
|
|||||||
@@ -29,25 +29,56 @@ import logger
|
|||||||
|
|
||||||
name = 'websocket'
|
name = 'websocket'
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||||
ws_reconnect = False
|
ws_shutdown = False
|
||||||
|
|
||||||
|
|
||||||
def start_thread():
|
def start_thread():
|
||||||
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
|
# Check for any existing sessions on start up
|
||||||
# Check for any existing sessions on start up
|
activity_pinger.check_active_sessions(ws_request=True)
|
||||||
activity_pinger.check_active_sessions(ws_request=True)
|
# Start the websocket listener on it's own thread
|
||||||
# Start the websocket listener on it's own thread
|
thread = threading.Thread(target=run)
|
||||||
threading.Thread(target=run).start()
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def on_connect():
|
||||||
|
if plexpy.PLEX_SERVER_UP is None:
|
||||||
|
plexpy.PLEX_SERVER_UP = True
|
||||||
|
|
||||||
|
if not plexpy.PLEX_SERVER_UP:
|
||||||
|
logger.info(u"Tautulli WebSocket :: The Plex Media Server is back up.")
|
||||||
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
||||||
|
plexpy.PLEX_SERVER_UP = True
|
||||||
|
|
||||||
|
plexpy.initialize_scheduler()
|
||||||
|
|
||||||
|
|
||||||
def on_disconnect():
|
def on_disconnect():
|
||||||
|
if plexpy.PLEX_SERVER_UP is None:
|
||||||
|
plexpy.PLEX_SERVER_UP = False
|
||||||
|
|
||||||
|
if plexpy.PLEX_SERVER_UP:
|
||||||
|
logger.info(u"Tautulli WebSocket :: Unable to get a response from the server, Plex server is down.")
|
||||||
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
||||||
|
plexpy.PLEX_SERVER_UP = False
|
||||||
|
|
||||||
activity_processor.ActivityProcessor().set_temp_stopped()
|
activity_processor.ActivityProcessor().set_temp_stopped()
|
||||||
plexpy.initialize_scheduler()
|
plexpy.initialize_scheduler()
|
||||||
|
|
||||||
|
|
||||||
def reconnect():
|
def reconnect():
|
||||||
global ws_reconnect
|
shutdown()
|
||||||
ws_reconnect = True
|
logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
|
||||||
|
start_thread()
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
global ws_shutdown
|
||||||
|
ws_shutdown = True
|
||||||
|
|
||||||
|
logger.info(u"Tautulli WebSocket :: Disconnecting websocket...")
|
||||||
|
plexpy.WEBSOCKET.close()
|
||||||
|
plexpy.WS_CONNECTED = False
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
@@ -55,7 +86,7 @@ def run():
|
|||||||
|
|
||||||
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
||||||
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||||
secure = ' secure'
|
secure = 'secure '
|
||||||
else:
|
else:
|
||||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||||
plexpy.CONFIG.PMS_IP,
|
plexpy.CONFIG.PMS_IP,
|
||||||
@@ -69,87 +100,76 @@ def run():
|
|||||||
else:
|
else:
|
||||||
header = []
|
header = []
|
||||||
|
|
||||||
global ws_reconnect
|
global ws_shutdown
|
||||||
ws_reconnect = False
|
ws_shutdown = False
|
||||||
reconnects = 0
|
reconnects = 0
|
||||||
ws_exception = False
|
|
||||||
|
|
||||||
# Try an open the websocket connection
|
# Try an open the websocket connection
|
||||||
while not plexpy.WS_CONNECTED and reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
while not plexpy.WS_CONNECTED and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||||
try:
|
if reconnects == 0:
|
||||||
logger.info(u"Tautulli WebSocket :: Opening%s websocket, connection attempt %s." % (secure, str(reconnects + 1)))
|
logger.info(u"Tautulli WebSocket :: Opening %swebsocket." % secure)
|
||||||
ws = create_connection(uri, header=header)
|
|
||||||
reconnects = 0
|
|
||||||
logger.info(u"Tautulli WebSocket :: Ready")
|
|
||||||
plexpy.WS_CONNECTED = True
|
|
||||||
|
|
||||||
if not plexpy.PLEX_SERVER_UP:
|
reconnects += 1
|
||||||
logger.info(u"Tautulli WebSocket :: The Plex Media Server is back up.")
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
|
||||||
plexpy.PLEX_SERVER_UP = True
|
|
||||||
|
|
||||||
plexpy.initialize_scheduler()
|
# Sleep 5 between connection attempts
|
||||||
|
if reconnects > 1:
|
||||||
except IOError as e:
|
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
|
||||||
reconnects += 1
|
|
||||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||||
|
|
||||||
except (websocket.WebSocketException, Exception) as e:
|
logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects))
|
||||||
|
|
||||||
|
try:
|
||||||
|
plexpy.WEBSOCKET = create_connection(uri, header=header)
|
||||||
|
logger.info(u"Tautulli WebSocket :: Ready")
|
||||||
|
plexpy.WS_CONNECTED = True
|
||||||
|
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
plexpy.WS_CONNECTED = False
|
|
||||||
ws_exception = True
|
if plexpy.WS_CONNECTED:
|
||||||
break
|
on_connect()
|
||||||
|
|
||||||
while plexpy.WS_CONNECTED:
|
while plexpy.WS_CONNECTED:
|
||||||
try:
|
try:
|
||||||
process(*receive(ws))
|
process(*receive(plexpy.WEBSOCKET))
|
||||||
|
|
||||||
# successfully received data, reset reconnects counter
|
# successfully received data, reset reconnects counter
|
||||||
reconnects = 0
|
reconnects = 0
|
||||||
|
|
||||||
except websocket.WebSocketConnectionClosedException:
|
except websocket.WebSocketConnectionClosedException:
|
||||||
if reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
if ws_shutdown:
|
||||||
|
break
|
||||||
|
|
||||||
|
if reconnects == 0:
|
||||||
|
logger.warn(u"Tautulli WebSocket :: Connection has closed.")
|
||||||
|
|
||||||
|
if not plexpy.CONFIG.PMS_IS_CLOUD and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||||
reconnects += 1
|
reconnects += 1
|
||||||
|
|
||||||
# Sleep 5 between connection attempts
|
# Sleep 5 between connection attempts
|
||||||
if reconnects > 1:
|
if reconnects > 1:
|
||||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||||
|
|
||||||
logger.warn(u"Tautulli WebSocket :: Connection has closed, reconnection attempt %s." % reconnects)
|
logger.warn(u"Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ws = create_connection(uri, header=header)
|
plexpy.WEBSOCKET = create_connection(uri, header=header)
|
||||||
logger.info(u"Tautulli WebSocket :: Ready")
|
logger.info(u"Tautulli WebSocket :: Ready")
|
||||||
plexpy.WS_CONNECTED = True
|
plexpy.WS_CONNECTED = True
|
||||||
except IOError as e:
|
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||||
logger.info(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ws.shutdown()
|
shutdown()
|
||||||
plexpy.WS_CONNECTED = False
|
|
||||||
break
|
break
|
||||||
|
|
||||||
except (websocket.WebSocketException, Exception) as e:
|
except (websocket.WebSocketException, Exception) as e:
|
||||||
|
if ws_shutdown:
|
||||||
|
break
|
||||||
|
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
plexpy.WS_CONNECTED = False
|
shutdown()
|
||||||
ws_exception = True
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if we recieved a restart notification and close websocket connection cleanly
|
if not plexpy.WS_CONNECTED and not ws_shutdown:
|
||||||
if ws_reconnect:
|
|
||||||
logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
|
|
||||||
ws.shutdown()
|
|
||||||
plexpy.WS_CONNECTED = False
|
|
||||||
start_thread()
|
|
||||||
|
|
||||||
if not plexpy.WS_CONNECTED and not ws_reconnect:
|
|
||||||
logger.error(u"Tautulli WebSocket :: Connection unavailable.")
|
|
||||||
|
|
||||||
if not ws_exception and plexpy.PLEX_SERVER_UP:
|
|
||||||
logger.info(u"Tautulli WebSocket :: Unable to get an internal response from the server, Plex server is down.")
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
|
||||||
plexpy.PLEX_SERVER_UP = False
|
|
||||||
|
|
||||||
on_disconnect()
|
on_disconnect()
|
||||||
|
|
||||||
logger.debug(u"Tautulli WebSocket :: Leaving thread.")
|
logger.debug(u"Tautulli WebSocket :: Leaving thread.")
|
||||||
@@ -180,7 +200,7 @@ def process(opcode, data):
|
|||||||
info = json.loads(data)
|
info = json.loads(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
|
logger.warn(u"Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
|
||||||
logger.debug(data)
|
logger.websocket_error(data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
info = info.get('NotificationContainer', info)
|
info = info.get('NotificationContainer', info)
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
# Form based authentication for CherryPy. Requires the
|
# Form based authentication for CherryPy. Requires the
|
||||||
# Session tool to be loaded.
|
# Session tool to be loaded.
|
||||||
|
|
||||||
from cgi import escape
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from hashing_passwords import check_hash
|
from hashing_passwords import check_hash
|
||||||
|
import jwt
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import logger
|
import logger
|
||||||
@@ -32,7 +32,9 @@ from plexpy.users import Users, refresh_users
|
|||||||
from plexpy.plextv import PlexTV
|
from plexpy.plextv import PlexTV
|
||||||
|
|
||||||
|
|
||||||
SESSION_KEY = '_cp_username'
|
JWT_ALGORITHM = 'HS256'
|
||||||
|
JWT_COOKIE_NAME = 'tautulli_token_'
|
||||||
|
|
||||||
|
|
||||||
def user_login(username=None, password=None):
|
def user_login(username=None, password=None):
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
@@ -52,10 +54,17 @@ def user_login(username=None, password=None):
|
|||||||
if user_id != str(user_details['user_id']):
|
if user_id != str(user_details['user_id']):
|
||||||
# The user is not in the database.
|
# The user is not in the database.
|
||||||
return None
|
return None
|
||||||
|
elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']:
|
||||||
|
# Plex admin login
|
||||||
|
return 'admin'
|
||||||
elif not user_details['allow_guest'] or user_details['deleted_user']:
|
elif not user_details['allow_guest'] or user_details['deleted_user']:
|
||||||
# Guest access is disabled or the user is deleted.
|
# Guest access is disabled or the user is deleted.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Stop here if guest access is not enabled
|
||||||
|
if not plexpy.CONFIG.ALLOW_GUEST_ACCESS:
|
||||||
|
return None
|
||||||
|
|
||||||
# The user is in the database, and guest access is enabled, so try to retrieve a server token.
|
# The user is in the database, and guest access is enabled, so try to retrieve a server token.
|
||||||
# If a server token is returned, then the user is a valid friend of the server.
|
# If a server token is returned, then the user is a valid friend of the server.
|
||||||
plex_tv = PlexTV(token=user_token)
|
plex_tv = PlexTV(token=user_token)
|
||||||
@@ -73,7 +82,7 @@ def user_login(username=None, password=None):
|
|||||||
# Refresh the users list to make sure we have all the correct permissions.
|
# Refresh the users list to make sure we have all the correct permissions.
|
||||||
refresh_users()
|
refresh_users()
|
||||||
# Successful login
|
# Successful login
|
||||||
return True
|
return 'guest'
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database." % username)
|
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database." % username)
|
||||||
return None
|
return None
|
||||||
@@ -89,37 +98,62 @@ def user_login(username=None, password=None):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_credentials(username, password, admin_login='0'):
|
def check_credentials(username, password, admin_login='0'):
|
||||||
"""Verifies credentials for username and password.
|
"""Verifies credentials for username and password.
|
||||||
Returns True and the user group on success or False and no user group"""
|
Returns True and the user group on success or False and no user group"""
|
||||||
|
|
||||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
if plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
return True, u'admin'
|
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
||||||
elif username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
return True, 'tautulli admin'
|
||||||
return True, u'admin'
|
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
elif not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS and user_login(username, password):
|
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
return True, u'guest'
|
return True, 'tautulli admin'
|
||||||
else:
|
|
||||||
return False, None
|
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
|
||||||
|
plex_login = user_login(username, password)
|
||||||
|
if plex_login is not None:
|
||||||
|
return True, plex_login
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def check_jwt_token():
|
||||||
|
jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID
|
||||||
|
jwt_token = cherrypy.request.cookie.get(jwt_cookie)
|
||||||
|
|
||||||
|
if jwt_token:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
jwt_token.value, plexpy.CONFIG.JWT_SECRET, leeway=timedelta(seconds=10), algorithms=[JWT_ALGORITHM]
|
||||||
|
)
|
||||||
|
except (jwt.DecodeError, jwt.ExpiredSignatureError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def check_auth(*args, **kwargs):
|
def check_auth(*args, **kwargs):
|
||||||
"""A tool that looks in config for 'auth.require'. If found and it
|
"""A tool that looks in config for 'auth.require'. If found and it
|
||||||
is not None, a login is required and the entry is evaluated as a list of
|
is not None, a login is required and the entry is evaluated as a list of
|
||||||
conditions that the user must fulfill"""
|
conditions that the user must fulfill"""
|
||||||
conditions = cherrypy.request.config.get('auth.require', None)
|
conditions = cherrypy.request.config.get('auth.require', None)
|
||||||
if conditions is not None:
|
if conditions is not None:
|
||||||
_session = cherrypy.session.get(SESSION_KEY)
|
payload = check_jwt_token()
|
||||||
|
|
||||||
|
if payload:
|
||||||
|
cherrypy.request.login = payload
|
||||||
|
|
||||||
if _session and (_session['user'] and _session['expiry']) and _session['expiry'] > datetime.now():
|
|
||||||
cherrypy.request.login = _session['user']
|
|
||||||
for condition in conditions:
|
for condition in conditions:
|
||||||
# A condition is just a callable that returns true or false
|
# A condition is just a callable that returns true or false
|
||||||
if not condition():
|
if not condition():
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/logout")
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/logout")
|
||||||
|
|
||||||
|
|
||||||
def requireAuth(*conditions):
|
def requireAuth(*conditions):
|
||||||
"""A decorator that appends conditions to the auth.require config
|
"""A decorator that appends conditions to the auth.require config
|
||||||
variable."""
|
variable."""
|
||||||
@@ -140,14 +174,13 @@ def requireAuth(*conditions):
|
|||||||
#
|
#
|
||||||
# Define those at will however suits the application.
|
# Define those at will however suits the application.
|
||||||
|
|
||||||
def member_of(groupname):
|
def member_of(user_group):
|
||||||
def check():
|
return lambda: cherrypy.request.login and cherrypy.request.login['user_group'] == user_group
|
||||||
# replace with actual check if <username> is in <groupname>
|
|
||||||
return cherrypy.request.login == plexpy.CONFIG.HTTP_USERNAME and groupname == 'admin'
|
|
||||||
return check
|
def name_is(user_name):
|
||||||
|
return lambda: cherrypy.request.login and cherrypy.request.login['user'] == user_name
|
||||||
|
|
||||||
def name_is(reqd_username):
|
|
||||||
return lambda: reqd_username == cherrypy.request.login
|
|
||||||
|
|
||||||
# These might be handy
|
# These might be handy
|
||||||
|
|
||||||
@@ -160,6 +193,7 @@ def any_of(*conditions):
|
|||||||
return False
|
return False
|
||||||
return check
|
return check
|
||||||
|
|
||||||
|
|
||||||
# By default all conditions are required, but this might still be
|
# By default all conditions are required, but this might still be
|
||||||
# needed if you want to use it inside of an any_of(...) condition
|
# needed if you want to use it inside of an any_of(...) condition
|
||||||
def all_of(*conditions):
|
def all_of(*conditions):
|
||||||
@@ -175,13 +209,18 @@ def all_of(*conditions):
|
|||||||
# Controller to provide login and logout actions
|
# Controller to provide login and logout actions
|
||||||
|
|
||||||
class AuthController(object):
|
class AuthController(object):
|
||||||
|
|
||||||
def on_login(self, user_id, username, user_group):
|
def check_auth_enabled(self):
|
||||||
|
if not plexpy.CONFIG.HTTP_BASIC_AUTH and plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
|
return
|
||||||
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
||||||
|
|
||||||
|
def on_login(self, username, user_id=None, user_group=None, success=0):
|
||||||
"""Called on successful login"""
|
"""Called on successful login"""
|
||||||
|
|
||||||
# Save login to the database
|
# Save login to the database
|
||||||
ip_address = cherrypy.request.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr'))
|
ip_address = cherrypy.request.remote.ip
|
||||||
host = cherrypy.request.headers.get('Origin')
|
host = cherrypy.request.base
|
||||||
user_agent = cherrypy.request.headers.get('User-Agent')
|
user_agent = cherrypy.request.headers.get('User-Agent')
|
||||||
|
|
||||||
Users().set_user_login(user_id=user_id,
|
Users().set_user_login(user_id=user_id,
|
||||||
@@ -190,86 +229,102 @@ class AuthController(object):
|
|||||||
ip_address=ip_address,
|
ip_address=ip_address,
|
||||||
host=host,
|
host=host,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
success=1)
|
success=success)
|
||||||
|
|
||||||
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username))
|
if success == 1:
|
||||||
|
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username))
|
||||||
|
|
||||||
def on_logout(self, username, user_group):
|
def on_logout(self, username, user_group):
|
||||||
"""Called on logout"""
|
"""Called on logout"""
|
||||||
logger.debug(u"Tautulli WebAuth :: %s User '%s' logged out of Tautulli." % (user_group.capitalize(), username))
|
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
|
||||||
|
|
||||||
def on_login_failed(self, username):
|
def get_loginform(self):
|
||||||
"""Called on failed login"""
|
|
||||||
|
|
||||||
# Save login attempt to the database
|
|
||||||
ip_address = cherrypy.request.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr'))
|
|
||||||
host = cherrypy.request.headers.get('Origin')
|
|
||||||
user_agent = cherrypy.request.headers.get('User-Agent')
|
|
||||||
|
|
||||||
Users().set_user_login(user=username,
|
|
||||||
ip_address=ip_address,
|
|
||||||
host=host,
|
|
||||||
user_agent=user_agent,
|
|
||||||
success=0)
|
|
||||||
|
|
||||||
def get_loginform(self, username="", msg=""):
|
|
||||||
from plexpy.webserve import serve_template
|
from plexpy.webserve import serve_template
|
||||||
return serve_template(templatename="login.html", title="Login", username=escape(username, True), msg=msg)
|
return serve_template(templatename="login.html", title="Login")
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def index(self):
|
def index(self):
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def login(self, username=None, password=None, remember_me='0', admin_login='0'):
|
def login(self):
|
||||||
if not cherrypy.config.get('tools.sessions.on'):
|
self.check_auth_enabled()
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
|
||||||
|
|
||||||
if not username and not password:
|
return self.get_loginform()
|
||||||
return self.get_loginform()
|
|
||||||
|
|
||||||
(vaild_login, user_group) = check_credentials(username, password, admin_login)
|
|
||||||
|
|
||||||
if vaild_login:
|
@cherrypy.expose
|
||||||
if user_group == 'guest':
|
def logout(self):
|
||||||
|
self.check_auth_enabled()
|
||||||
|
|
||||||
|
payload = check_jwt_token()
|
||||||
|
if payload:
|
||||||
|
self.on_logout(payload['user'], payload['user_group'])
|
||||||
|
|
||||||
|
jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID
|
||||||
|
cherrypy.response.cookie[jwt_cookie] = 'expire'
|
||||||
|
cherrypy.response.cookie[jwt_cookie]['expires'] = 0
|
||||||
|
cherrypy.response.cookie[jwt_cookie]['path'] = '/'
|
||||||
|
|
||||||
|
cherrypy.request.login = None
|
||||||
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def signin(self, username=None, password=None, remember_me='0', admin_login='0'):
|
||||||
|
if cherrypy.request.method != 'POST':
|
||||||
|
cherrypy.response.status = 405
|
||||||
|
return {'status': 'error', 'message': 'Sign in using POST.'}
|
||||||
|
|
||||||
|
error_message = {'status': 'error', 'message': 'Incorrect username or password.'}
|
||||||
|
|
||||||
|
valid_login, user_group = check_credentials(username, password, admin_login)
|
||||||
|
|
||||||
|
if valid_login:
|
||||||
|
if user_group == 'tautulli admin':
|
||||||
|
user_group = 'admin'
|
||||||
|
user_id = None
|
||||||
|
else:
|
||||||
if re.match(r"[^@]+@[^@]+\.[^@]+", username):
|
if re.match(r"[^@]+@[^@]+\.[^@]+", username):
|
||||||
user_details = Users().get_details(email=username)
|
user_details = Users().get_details(email=username)
|
||||||
else:
|
else:
|
||||||
user_details = Users().get_details(user=username)
|
user_details = Users().get_details(user=username)
|
||||||
|
|
||||||
user_id = user_details['user_id']
|
user_id = user_details['user_id']
|
||||||
else:
|
|
||||||
user_id = None
|
|
||||||
|
|
||||||
expiry = datetime.now() + (timedelta(days=30) if remember_me == '1' else timedelta(minutes=60))
|
time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)
|
||||||
|
expiry = datetime.utcnow() + time_delta
|
||||||
|
|
||||||
cherrypy.request.login = username
|
payload = {
|
||||||
cherrypy.session[SESSION_KEY] = {'user_id': user_id,
|
'user_id': user_id,
|
||||||
'user': username,
|
'user': username,
|
||||||
'user_group': user_group,
|
'user_group': user_group,
|
||||||
'expiry': expiry}
|
'exp': expiry
|
||||||
|
}
|
||||||
|
|
||||||
self.on_login(user_id, username, user_group)
|
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
|
||||||
|
self.on_login(username=username,
|
||||||
|
user_id=user_id,
|
||||||
|
user_group=user_group,
|
||||||
|
success=1)
|
||||||
|
|
||||||
|
jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID
|
||||||
|
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
||||||
|
cherrypy.response.cookie[jwt_cookie]['expires'] = int(time_delta.total_seconds())
|
||||||
|
cherrypy.response.cookie[jwt_cookie]['path'] = '/'
|
||||||
|
|
||||||
|
cherrypy.request.login = payload
|
||||||
|
cherrypy.response.status = 200
|
||||||
|
return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID}
|
||||||
|
|
||||||
elif admin_login == '1':
|
elif admin_login == '1':
|
||||||
self.on_login_failed(username)
|
self.on_login(username=username)
|
||||||
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
|
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
cherrypy.response.status = 401
|
||||||
|
return error_message
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.on_login_failed(username)
|
self.on_login(username=username)
|
||||||
logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username)
|
logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username)
|
||||||
return self.get_loginform(username, u"Incorrect username/email or password.")
|
cherrypy.response.status = 401
|
||||||
|
return error_message
|
||||||
@cherrypy.expose
|
|
||||||
def logout(self):
|
|
||||||
if not cherrypy.config.get('tools.sessions.on'):
|
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
|
||||||
|
|
||||||
_session = cherrypy.session.get(SESSION_KEY)
|
|
||||||
cherrypy.session[SESSION_KEY] = None
|
|
||||||
|
|
||||||
if _session and _session['user']:
|
|
||||||
cherrypy.request.login = None
|
|
||||||
self.on_logout(_session['user'], _session['user_group'])
|
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,8 @@ def initialize(options):
|
|||||||
if enable_https:
|
if enable_https:
|
||||||
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
||||||
if plexpy.CONFIG.HTTPS_CREATE_CERT and \
|
if plexpy.CONFIG.HTTPS_CREATE_CERT and \
|
||||||
(not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key))):
|
(not (https_cert and os.path.exists(https_cert)) or
|
||||||
|
not (https_key and os.path.exists(https_key))):
|
||||||
if not create_https_certificates(https_cert, https_key):
|
if not create_https_certificates(https_cert, https_key):
|
||||||
logger.warn(u"Tautulli WebStart :: Unable to create certificate and key. Disabling HTTPS")
|
logger.warn(u"Tautulli WebStart :: Unable to create certificate and key. Disabling HTTPS")
|
||||||
enable_https = False
|
enable_https = False
|
||||||
@@ -67,16 +68,21 @@ def initialize(options):
|
|||||||
protocol = "http"
|
protocol = "http"
|
||||||
|
|
||||||
if options['http_password']:
|
if options['http_password']:
|
||||||
logger.info(u"Tautulli WebStart :: Web server authentication is enabled, username is '%s'", options['http_username'])
|
login_allowed = ["Tautulli admin (username is '%s')" % options['http_username']]
|
||||||
|
if plexpy.CONFIG.HTTP_PLEX_ADMIN:
|
||||||
|
login_allowed.append("Plex admin")
|
||||||
|
|
||||||
|
logger.info(u"Tautulli WebStart :: Web server authentication is enabled: %s.", ' and '.join(login_allowed))
|
||||||
|
|
||||||
if options['http_basic_auth']:
|
if options['http_basic_auth']:
|
||||||
session_enabled = auth_enabled = False
|
auth_enabled = False
|
||||||
basic_auth_enabled = True
|
basic_auth_enabled = True
|
||||||
else:
|
else:
|
||||||
options_dict['tools.sessions.on'] = session_enabled = auth_enabled = True
|
auth_enabled = True
|
||||||
basic_auth_enabled = False
|
basic_auth_enabled = False
|
||||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
|
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
|
||||||
else:
|
else:
|
||||||
session_enabled = auth_enabled = basic_auth_enabled = False
|
auth_enabled = basic_auth_enabled = False
|
||||||
|
|
||||||
if options['http_root'].strip('/'):
|
if options['http_root'].strip('/'):
|
||||||
plexpy.HTTP_ROOT = options['http_root'] = '/' + options['http_root'].strip('/') + '/'
|
plexpy.HTTP_ROOT = options['http_root'] = '/' + options['http_root'].strip('/') + '/'
|
||||||
@@ -93,11 +99,6 @@ def initialize(options):
|
|||||||
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
||||||
'text/javascript', 'application/json',
|
'text/javascript', 'application/json',
|
||||||
'application/javascript'],
|
'application/javascript'],
|
||||||
'tools.sessions.on': session_enabled,
|
|
||||||
'tools.session.name': 'tautulli_session_id-' + plexpy.CONFIG.PMS_UUID,
|
|
||||||
'tools.sessions.storage_type': 'file',
|
|
||||||
'tools.sessions.storage_path': plexpy.CONFIG.CACHE_DIR,
|
|
||||||
'tools.sessions.timeout': 30 * 24 * 60, # 30 days
|
|
||||||
'tools.auth.on': auth_enabled,
|
'tools.auth.on': auth_enabled,
|
||||||
'tools.auth_basic.on': basic_auth_enabled,
|
'tools.auth_basic.on': basic_auth_enabled,
|
||||||
'tools.auth_basic.realm': 'Tautulli web server',
|
'tools.auth_basic.realm': 'Tautulli web server',
|
||||||
@@ -121,6 +122,7 @@ def initialize(options):
|
|||||||
'/images': {
|
'/images': {
|
||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
'tools.staticdir.dir': "interfaces/default/images",
|
'tools.staticdir.dir': "interfaces/default/images",
|
||||||
|
'tools.staticdir.content_types': {'svg': 'image/svg+xml'},
|
||||||
'tools.caching.on': True,
|
'tools.caching.on': True,
|
||||||
'tools.caching.force': True,
|
'tools.caching.force': True,
|
||||||
'tools.caching.delay': 0,
|
'tools.caching.delay': 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user