Compare commits
71 Commits
v2.0.15-be
...
v2.0.19-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
770f12b632 |
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,5 +1,68 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## v2.0.15-beta (2018-01-27)
|
||||||
|
|
||||||
* 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.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
@@ -33,7 +33,7 @@ import signal
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import config, database, logger, web_socket, webstart
|
from plexpy import config, database, logger, webstart
|
||||||
|
|
||||||
|
|
||||||
# Register signals, such as CTRL + C
|
# Register signals, such as CTRL + C
|
||||||
@@ -194,13 +194,6 @@ def main():
|
|||||||
# Start the background threads
|
# Start the background threads
|
||||||
plexpy.start()
|
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
|
# Force the http port if neccessary
|
||||||
if args.port:
|
if args.port:
|
||||||
http_port = args.port
|
http_port = args.port
|
||||||
|
53
README.md
53
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://discord.gg/tQcWEUp)
|
[](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/tQcWEUp), [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
|
||||||
|
|
||||||
|
@@ -44,16 +44,18 @@
|
|||||||
% 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.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
||||||
<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/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
||||||
newer version</a> is available.<br />
|
newer version</a> is available!<br />
|
||||||
You're ${plexpy.COMMITS_BEHIND} commits behind.<br />
|
You are ${plexpy.COMMITS_BEHIND} commits behind.<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>
|
||||||
|
% else:
|
||||||
|
<div id="updatebar" style="display: none;"></div>
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
<nav class="navbar navbar-fixed-top">
|
<nav class="navbar navbar-fixed-top">
|
||||||
@@ -125,7 +127,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>
|
||||||
@@ -239,7 +241,7 @@ ${next.modalIncludes()}
|
|||||||
<p>
|
<p>
|
||||||
Click the button below to continue to Flattr.
|
Click the button below to continue to Flattr.
|
||||||
</p>
|
</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">
|
<a href="${anon_url('https://flattr.com/submit/auto?user_id=JonnyWong16&url=https://github.com/%s/%s&title=Tautulli&language=en_GB&tags=github&category=software' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">
|
||||||
<img src="images/flattr-badge-large.png" alt="Flattr">
|
<img src="images/flattr-badge-large.png" alt="Flattr">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,14 +291,44 @@ ${next.modalIncludes()}
|
|||||||
% endif
|
% 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 === true) {
|
||||||
|
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> is available!<br />' +
|
||||||
|
'You are '+ result.commits_behind + ' commits 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);
|
||||||
|
} else 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_callback) {
|
||||||
|
_callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#nav-shutdown").click(function() {
|
$("#nav-shutdown").click(function() {
|
||||||
@@ -315,11 +347,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 () {
|
||||||
|
@@ -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.CONFIG.GIT_BRANCH))}">${plexpy.CURRENT_VERSION}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
@@ -75,10 +75,10 @@ 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>
|
||||||
@@ -86,7 +86,7 @@ DOCUMENTATION :: END
|
|||||||
<td>
|
<td>
|
||||||
<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://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>
|
||||||
|
@@ -60,7 +60,8 @@ 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;
|
||||||
@@ -80,12 +81,37 @@ select.form-control {
|
|||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
.selectize-control.form-control .selectize-input {
|
.selectize-control.form-control .selectize-input {
|
||||||
display: flex;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding-left: 5px;
|
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;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
@@ -108,6 +134,9 @@ select.form-control {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 10px;
|
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 {
|
.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;
|
||||||
@@ -131,33 +160,40 @@ select.form-control:focus,
|
|||||||
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
||||||
fill: #999 !important;
|
fill: #999 !important;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-value {
|
.selectize-input > div .item-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-value {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-text + .item-value {
|
.selectize-input > div .item-text + .item-value {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-value:before {
|
.selectize-input > div .item-value:before {
|
||||||
content: '<';
|
content: '<';
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-value:after {
|
.selectize-input > div .item-value:after {
|
||||||
content: '>';
|
content: '>';
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .caption {
|
.selectize-dropdown .caption {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: block;
|
display: block;
|
||||||
color: #a0a0a0;
|
color: #a0a0a0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .select-all,
|
.selectize-dropdown .select-all,
|
||||||
.selectize-control .selectize-dropdown .remove-all {
|
.selectize-dropdown .remove-all {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .border-all {
|
.selectize-dropdown .border-all {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: block;
|
display: block;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -166,7 +202,7 @@ select.form-control:focus,
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #e5e5e5;
|
background-color: #e5e5e5;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .border-all:last-child {
|
.selectize-dropdown .border-all:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.selectize-dropdown .optgroup-header {
|
.selectize-dropdown .optgroup-header {
|
||||||
@@ -616,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;
|
||||||
@@ -2134,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 {
|
||||||
@@ -3195,16 +3235,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;
|
||||||
}
|
}
|
||||||
|
@@ -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'):
|
||||||
@@ -269,8 +276,9 @@ 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':
|
||||||
<span id="location-${sk}">${data['location'].upper()}</span>: <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']}">
|
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
||||||
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
||||||
</a>
|
</a>
|
||||||
@@ -352,13 +360,9 @@ DOCUMENTATION :: END
|
|||||||
</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':
|
||||||
@@ -371,21 +375,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>
|
||||||
@@ -425,10 +429,10 @@ DOCUMENTATION :: END
|
|||||||
% 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:
|
||||||
@@ -453,11 +457,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>
|
||||||
|
@@ -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_id']}" == "None" ? 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':
|
||||||
|
@@ -88,8 +88,9 @@ 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']}">
|
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else '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=300&fallback=${type});"></div>
|
||||||
% else:
|
% else:
|
||||||
@@ -98,7 +99,8 @@ DOCUMENTATION :: END
|
|||||||
</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 {
|
||||||
|
@@ -309,14 +309,17 @@
|
|||||||
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) {
|
if (lan_bw) {
|
||||||
bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
||||||
}
|
}
|
||||||
if (wan_bw) {
|
if (wan_bw) {
|
||||||
bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
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(/, $/, '') + ')';
|
||||||
}
|
}
|
||||||
bandwidth_header = bandwidth_header.replace(/, $/, '') + ')';
|
|
||||||
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
||||||
|
|
||||||
$('#currentActivityHeader').show();
|
$('#currentActivityHeader').show();
|
||||||
|
@@ -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>
|
||||||
@@ -706,13 +716,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>
|
||||||
|
@@ -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,9 @@ 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) {
|
return (seconds == 60 ? (minutes+1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
|
||||||
output = Math.round(minutes, 0)
|
|
||||||
} else {
|
|
||||||
minutesFloor = Math.floor(minutes);
|
|
||||||
secondsReal = Math.round((seconds - (minutesFloor * 60)), 0);
|
|
||||||
if (secondsReal < 10) {
|
|
||||||
secondsReal = '0' + secondsReal;
|
|
||||||
}
|
|
||||||
output = minutesFloor + ':' + secondsReal;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
} 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",
|
||||||
@@ -322,7 +322,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 () {
|
||||||
|
@@ -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');
|
||||||
@@ -144,14 +144,16 @@ sync_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)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$('#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'] });
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -163,7 +163,7 @@
|
|||||||
<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 conditions to only <strong>allow certain notifications</strong>. By default, all notifications will be sent if there are no 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>
|
||||||
<div id="condition-widget"></div>
|
<div id="condition-widget"></div>
|
||||||
@@ -342,7 +342,22 @@
|
|||||||
$('#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',
|
||||||
@@ -503,7 +518,6 @@
|
|||||||
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[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({
|
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
|
||||||
plugins: ['remove_button'],
|
plugins: ['remove_button'],
|
||||||
persist: false,
|
|
||||||
maxItems: null,
|
maxItems: null,
|
||||||
render: {
|
render: {
|
||||||
item: function(item, escape) {
|
item: function(item, escape) {
|
||||||
@@ -580,6 +594,18 @@
|
|||||||
});
|
});
|
||||||
var join_device_names = $join_device_names[0].selectize;
|
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});
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
function validateLogic() {
|
function validateLogic() {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,16 @@
|
|||||||
</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>
|
||||||
@@ -87,17 +97,45 @@
|
|||||||
<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_id']}" == "None" ? null : "${_session['user_id']}";
|
||||||
|
loadSyncTable(selected_user_id);
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('#row-edit-mode').on('click', function() {
|
$('#row-edit-mode').on('click', function() {
|
||||||
|
@@ -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>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<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;">
|
||||||
@@ -82,22 +82,26 @@
|
|||||||
</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 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>
|
||||||
@@ -105,20 +109,23 @@
|
|||||||
<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" 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" 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" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
|
||||||
|
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
<input type="hidden" class="form-control pms-settings" 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,19 @@
|
|||||||
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,
|
||||||
@@ -444,39 +496,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>
|
||||||
|
@@ -15,12 +15,14 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
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
|
||||||
@@ -91,7 +93,7 @@ HTTP_ROOT = None
|
|||||||
DEV = False
|
DEV = False
|
||||||
|
|
||||||
WS_CONNECTED = False
|
WS_CONNECTED = False
|
||||||
PLEX_SERVER_UP = True
|
PLEX_SERVER_UP = None
|
||||||
|
|
||||||
|
|
||||||
def initialize(config_file):
|
def initialize(config_file):
|
||||||
@@ -202,6 +204,8 @@ def initialize(config_file):
|
|||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(u"Unable to read previous version from file '%s': %s" %
|
logger.error(u"Unable to read previous version from file '%s': %s" %
|
||||||
(version_lock_file, e))
|
(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.
|
||||||
@@ -273,6 +277,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(
|
||||||
@@ -382,7 +387,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 for 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:
|
||||||
@@ -400,12 +405,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 for 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()):
|
||||||
@@ -449,6 +451,9 @@ def start():
|
|||||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||||
notifiers.check_browser_enabled()
|
notifiers.check_browser_enabled()
|
||||||
|
|
||||||
|
if CONFIG.FIRST_RUN_COMPLETE:
|
||||||
|
activity_pinger.connect_server(log=True, startup=True)
|
||||||
|
|
||||||
_STARTED = True
|
_STARTED = True
|
||||||
|
|
||||||
|
|
||||||
@@ -596,7 +601,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
|
||||||
@@ -1069,9 +1074,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
|
||||||
@@ -1230,7 +1235,6 @@ 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
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
try:
|
try:
|
||||||
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
||||||
@@ -1561,29 +1565,40 @@ 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)
|
SCHED.shutdown(wait=False)
|
||||||
|
@@ -97,14 +97,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()
|
||||||
@@ -121,7 +122,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_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()
|
||||||
@@ -158,7 +159,7 @@ class ActivityHandler(object):
|
|||||||
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():
|
||||||
@@ -177,7 +178,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_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():
|
||||||
@@ -215,7 +216,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):
|
||||||
@@ -230,7 +231,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'])
|
||||||
@@ -272,13 +273,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.
|
||||||
@@ -287,7 +300,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):
|
||||||
@@ -370,7 +383,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'):
|
||||||
@@ -386,7 +399,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:
|
||||||
@@ -397,7 +410,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
|
||||||
@@ -427,7 +440,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)
|
||||||
@@ -437,7 +450,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)))
|
||||||
|
|
||||||
|
|
||||||
@@ -448,7 +461,7 @@ def force_stop_stream(session_key):
|
|||||||
row_id = ap.write_session_history(session=session)
|
row_id = ap.write_session_history(session=session)
|
||||||
|
|
||||||
if row_id:
|
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(row_id=row_id)
|
ap.delete_session(row_id=row_id)
|
||||||
@@ -464,7 +477,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:
|
||||||
@@ -545,7 +558,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,9 +155,9 @@ 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
|
||||||
row_id = monitor_process.write_session_history(session=stream)
|
row_id = monitor_process.write_session_history(session=stream)
|
||||||
@@ -243,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'])
|
||||||
@@ -261,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():
|
||||||
@@ -325,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())
|
||||||
@@ -235,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,
|
||||||
@@ -260,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
|
||||||
@@ -304,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'],
|
||||||
@@ -371,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
|
||||||
@@ -381,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'],
|
||||||
@@ -417,7 +422,8 @@ 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 the session row id when the session is successfully written to the database
|
# Return the session row id when the session is successfully written to the database
|
||||||
|
@@ -335,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):
|
||||||
|
@@ -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', ''),
|
||||||
@@ -480,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', ''),
|
||||||
@@ -876,3 +877,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
|
||||||
|
@@ -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:
|
||||||
@@ -1107,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:
|
||||||
@@ -1118,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 ' \
|
||||||
@@ -1128,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)
|
||||||
|
|
||||||
@@ -1143,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()
|
||||||
|
|
||||||
|
@@ -698,6 +698,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'])
|
||||||
@@ -729,16 +733,18 @@ class Graphs(object):
|
|||||||
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" ' \
|
||||||
@@ -758,16 +764,18 @@ class Graphs(object):
|
|||||||
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, ' \
|
||||||
@@ -799,6 +807,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'])
|
||||||
|
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
@@ -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.VERSION_NUMBER,
|
||||||
|
'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 = []
|
||||||
|
@@ -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])
|
||||||
|
|
||||||
@@ -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
|
||||||
|
@@ -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):
|
||||||
@@ -277,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)))
|
||||||
@@ -318,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)
|
||||||
@@ -390,6 +385,28 @@ def get_notify_state(session):
|
|||||||
return notify_states
|
return notify_states
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
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):
|
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
||||||
|
|
||||||
if notifier and notify_action:
|
if notifier and notify_action:
|
||||||
@@ -531,7 +548,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
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'
|
||||||
|
|
||||||
@@ -542,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'
|
||||||
|
|
||||||
@@ -562,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)
|
||||||
@@ -570,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)
|
||||||
@@ -839,6 +870,8 @@ 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','')
|
||||||
|
|
||||||
|
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
||||||
|
|
||||||
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', {}))
|
||||||
|
|
||||||
@@ -864,7 +897,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||||||
'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'],
|
||||||
@@ -978,8 +1011,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:
|
||||||
@@ -1037,14 +1070,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)
|
||||||
@@ -1194,6 +1230,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:
|
||||||
|
@@ -90,7 +90,8 @@ AGENT_IDS = {'growl': 0,
|
|||||||
'discord': 20,
|
'discord': 20,
|
||||||
'androidapp': 21,
|
'androidapp': 21,
|
||||||
'groupme': 22,
|
'groupme': 22,
|
||||||
'mqtt': 23
|
'mqtt': 23,
|
||||||
|
'zapier': 24
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -186,6 +187,10 @@ def available_notification_agents():
|
|||||||
{'label': 'XBMC',
|
{'label': 'XBMC',
|
||||||
'name': 'xbmc',
|
'name': 'xbmc',
|
||||||
'id': AGENT_IDS['xbmc']
|
'id': AGENT_IDS['xbmc']
|
||||||
|
},
|
||||||
|
{'label': 'Zapier',
|
||||||
|
'name': 'zapier',
|
||||||
|
'id': AGENT_IDS['zapier']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -316,7 +321,7 @@ def available_notification_actions():
|
|||||||
'name': 'on_plexpyupdate',
|
'name': 'on_plexpyupdate',
|
||||||
'description': 'Trigger a notification when an update for the Tautulli is available.',
|
'description': 'Trigger a notification when an update for the Tautulli is available.',
|
||||||
'subject': 'Tautulli ({server_name})',
|
'subject': 'Tautulli ({server_name})',
|
||||||
'body': 'An update is available for Tautulli (version {plexpy_update_version}).',
|
'body': 'An update is available for Tautulli (version {tautulli_update_version}).',
|
||||||
'icon': 'fa-refresh',
|
'icon': 'fa-refresh',
|
||||||
'media_types': ('server',)
|
'media_types': ('server',)
|
||||||
}
|
}
|
||||||
@@ -377,6 +382,8 @@ def get_agent_class(agent_id=None, config=None):
|
|||||||
return GROUPME(config=config)
|
return GROUPME(config=config)
|
||||||
elif agent_id == 23:
|
elif agent_id == 23:
|
||||||
return MQTT(config=config)
|
return MQTT(config=config)
|
||||||
|
elif agent_id == 24:
|
||||||
|
return ZAPIER(config=config)
|
||||||
else:
|
else:
|
||||||
return Notifier(config=config)
|
return Notifier(config=config)
|
||||||
else:
|
else:
|
||||||
@@ -652,13 +659,28 @@ class PrettyMetadata(object):
|
|||||||
provider_name = 'Trakt.tv'
|
provider_name = 'Trakt.tv'
|
||||||
elif provider == 'lastfm':
|
elif provider == 'lastfm':
|
||||||
provider_name = 'Last.fm'
|
provider_name = 'Last.fm'
|
||||||
|
else:
|
||||||
|
if self.media_type == 'movie':
|
||||||
|
provider_name = 'IMDb'
|
||||||
|
elif self.media_type in ('show', 'season', 'episode'):
|
||||||
|
provider_name = 'TheTVDB'
|
||||||
|
elif self.media_type in ('artist', 'album', 'track'):
|
||||||
|
provider_name = 'Last.fm'
|
||||||
return provider_name
|
return provider_name
|
||||||
|
|
||||||
def get_provider_link(self, provider=None):
|
def get_provider_link(self, provider=None):
|
||||||
|
provider_link = ''
|
||||||
if provider == 'plexweb':
|
if provider == 'plexweb':
|
||||||
provider_link = self.get_plex_url()
|
provider_link = self.get_plex_url()
|
||||||
else:
|
elif provider:
|
||||||
provider_link = self.parameters.get(provider + '_url', '')
|
provider_link = self.parameters.get(provider + '_url', '')
|
||||||
|
else:
|
||||||
|
if self.media_type == 'movie':
|
||||||
|
provider_link = self.parameters.get('imdb_url', '')
|
||||||
|
elif self.media_type in ('show', 'season', 'episode'):
|
||||||
|
provider_link = self.parameters.get('thetvdb_url', '')
|
||||||
|
elif self.media_type in ('artist', 'album', 'track'):
|
||||||
|
provider_link = self.parameters.get('lastfm_url', '')
|
||||||
return provider_link
|
return provider_link
|
||||||
|
|
||||||
def get_caption(self, provider):
|
def get_caption(self, provider):
|
||||||
@@ -874,7 +896,8 @@ class ANDROIDAPP(Notifier):
|
|||||||
'The content of your notifications will be sent unencrypted!</strong><br>' \
|
'The content of your notifications will be sent unencrypted!</strong><br>' \
|
||||||
'Please install the library to encrypt the notification contents. ' \
|
'Please install the library to encrypt the notification contents. ' \
|
||||||
'Instructions can be found in the ' \
|
'Instructions can be found in the ' \
|
||||||
'<a href="' + helpers.anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)#notifications-pycryptodome' % plexpy.CONFIG.GIT_USER) + '" target="_blank">FAQ</a>.',
|
'<a href="' + helpers.anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
|
||||||
|
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.',
|
||||||
'input_type': 'help'
|
'input_type': 'help'
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
@@ -1919,23 +1942,24 @@ class IFTTT(Notifier):
|
|||||||
headers=headers, json=data)
|
headers=headers, json=data)
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'Ifttt Maker Channel Key',
|
config_option = [{'label': 'IFTTT Webhook Key',
|
||||||
'value': self.config['key'],
|
'value': self.config['key'],
|
||||||
'name': 'ifttt_key',
|
'name': 'ifttt_key',
|
||||||
'description': 'Your Ifttt key. You can get a key from'
|
'description': 'Your IFTTT webhook key. You can get a key from'
|
||||||
' <a href="' + helpers.anon_url('https://ifttt.com/maker') + '" target="_blank">here</a>.',
|
' <a href="' + helpers.anon_url('https://ifttt.com/maker_webhooks') + '" target="_blank">here</a>.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Ifttt Event',
|
{'label': 'IFTTT Event',
|
||||||
'value': self.config['event'],
|
'value': self.config['event'],
|
||||||
'name': 'ifttt_event',
|
'name': 'ifttt_event',
|
||||||
'description': 'The Ifttt maker event to fire. You can include'
|
'description': 'The IFTTT maker event to fire. You can include'
|
||||||
' the {action} to be substituted with the action name.'
|
' <span class="inline-pre">{action}</span>'
|
||||||
|
' to be substituted with the action name.'
|
||||||
' The notification subject and body will be sent'
|
' The notification subject and body will be sent'
|
||||||
' as value1 and value2 respectively.',
|
' as <span class="inline-pre">value1</span>'
|
||||||
|
' and <span class="inline-pre">value2</span> respectively.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return config_option
|
return config_option
|
||||||
@@ -2072,7 +2096,7 @@ class JOIN(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'join_movie_provider',
|
'name': 'join_movie_provider',
|
||||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>'
|
||||||
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -2080,7 +2104,7 @@ class JOIN(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'join_tv_provider',
|
'name': 'join_tv_provider',
|
||||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>'
|
||||||
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -2088,7 +2112,7 @@ class JOIN(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'join_music_provider',
|
'name': 'join_music_provider',
|
||||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
'description': 'Select the source for music links in the notificaation. Leave blank for default.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -2714,7 +2738,7 @@ class PUSHOVER(Notifier):
|
|||||||
{'label': 'Movie Link Source',
|
{'label': 'Movie Link Source',
|
||||||
'value': self.config['movie_provider'],
|
'value': self.config['movie_provider'],
|
||||||
'name': 'pushover_movie_provider',
|
'name': 'pushover_movie_provider',
|
||||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
|
||||||
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_movie_providers()
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
@@ -2722,7 +2746,7 @@ class PUSHOVER(Notifier):
|
|||||||
{'label': 'TV Show Link Source',
|
{'label': 'TV Show Link Source',
|
||||||
'value': self.config['tv_provider'],
|
'value': self.config['tv_provider'],
|
||||||
'name': 'pushover_tv_provider',
|
'name': 'pushover_tv_provider',
|
||||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
|
||||||
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_tv_providers()
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
@@ -2730,7 +2754,7 @@ class PUSHOVER(Notifier):
|
|||||||
{'label': 'Music Link Source',
|
{'label': 'Music Link Source',
|
||||||
'value': self.config['music_provider'],
|
'value': self.config['music_provider'],
|
||||||
'name': 'pushover_music_provider',
|
'name': 'pushover_music_provider',
|
||||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
'description': 'Select the source for music links in the notification. Leave blank for default.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': PrettyMetadata().get_music_providers()
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
}
|
}
|
||||||
@@ -3403,6 +3427,104 @@ class XBMC(Notifier):
|
|||||||
return config_option
|
return config_option
|
||||||
|
|
||||||
|
|
||||||
|
class ZAPIER(Notifier):
|
||||||
|
"""
|
||||||
|
Zapier notifications
|
||||||
|
"""
|
||||||
|
NAME = 'Zapier'
|
||||||
|
_DEFAULT_CONFIG = {'hook': '',
|
||||||
|
'movie_provider': '',
|
||||||
|
'tv_provider': '',
|
||||||
|
'music_provider': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
def _test_hook(self):
|
||||||
|
_test_data = {'subject': 'Subject',
|
||||||
|
'body': 'Body',
|
||||||
|
'action': 'Action',
|
||||||
|
'poster_url': 'https://i.imgur.com',
|
||||||
|
'provider_name': 'Provider Name',
|
||||||
|
'provider_link': 'http://www.imdb.com',
|
||||||
|
'plex_url': 'https://app.plex.tv/desktop'}
|
||||||
|
|
||||||
|
return self.agent_notify(_test_data=_test_data)
|
||||||
|
|
||||||
|
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||||
|
data = {'subject': subject.encode("utf-8"),
|
||||||
|
'body': body.encode("utf-8"),
|
||||||
|
'action': action.encode("utf-8")}
|
||||||
|
|
||||||
|
if kwargs.get('parameters', {}).get('media_type'):
|
||||||
|
# Grab formatted metadata
|
||||||
|
pretty_metadata = PrettyMetadata(kwargs['parameters'])
|
||||||
|
|
||||||
|
if pretty_metadata.media_type == 'movie':
|
||||||
|
provider = self.config['movie_provider']
|
||||||
|
elif pretty_metadata.media_type in ('show', 'season', 'episode'):
|
||||||
|
provider = self.config['tv_provider']
|
||||||
|
elif pretty_metadata.media_type in ('artist', 'album', 'track'):
|
||||||
|
provider = self.config['music_provider']
|
||||||
|
else:
|
||||||
|
provider = None
|
||||||
|
|
||||||
|
poster_url = pretty_metadata.get_poster_url()
|
||||||
|
provider_name = pretty_metadata.get_provider_name(provider)
|
||||||
|
provider_link = pretty_metadata.get_provider_link(provider)
|
||||||
|
plex_url = pretty_metadata.get_plex_url()
|
||||||
|
|
||||||
|
data['poster_url'] = poster_url
|
||||||
|
data['provider_name'] = provider_name
|
||||||
|
data['provider_link'] = provider_link
|
||||||
|
data['plex_url'] = plex_url
|
||||||
|
|
||||||
|
if kwargs.get('_test_data'):
|
||||||
|
data.update(kwargs['_test_data'])
|
||||||
|
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
|
||||||
|
return self.make_request(self.config['hook'], headers=headers, json=data)
|
||||||
|
|
||||||
|
def return_config_options(self):
|
||||||
|
config_option = [{'label': 'Zapier Webhook URL',
|
||||||
|
'value': self.config['hook'],
|
||||||
|
'name': 'zapier_hook',
|
||||||
|
'description': 'Your Zapier webhook URL.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Test Zapier Webhook',
|
||||||
|
'value': 'Send Test Data',
|
||||||
|
'name': 'zapier_test_hook',
|
||||||
|
'description': 'Click this button when prompted on then "Test Webhooks by Zapier" step.',
|
||||||
|
'input_type': 'button'
|
||||||
|
},
|
||||||
|
{'label': 'Movie Link Source',
|
||||||
|
'value': self.config['movie_provider'],
|
||||||
|
'name': 'zapier_movie_provider',
|
||||||
|
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
|
||||||
|
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
|
'input_type': 'select',
|
||||||
|
'select_options': PrettyMetadata().get_movie_providers()
|
||||||
|
},
|
||||||
|
{'label': 'TV Show Link Source',
|
||||||
|
'value': self.config['tv_provider'],
|
||||||
|
'name': 'zapier_tv_provider',
|
||||||
|
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
|
||||||
|
'3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||||
|
'input_type': 'select',
|
||||||
|
'select_options': PrettyMetadata().get_tv_providers()
|
||||||
|
},
|
||||||
|
{'label': 'Music Link Source',
|
||||||
|
'value': self.config['music_provider'],
|
||||||
|
'name': 'zapier_music_provider',
|
||||||
|
'description': 'Select the source for music links in the notification. Leave blank for default.',
|
||||||
|
'input_type': 'select',
|
||||||
|
'select_options': PrettyMetadata().get_music_providers()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return config_option
|
||||||
|
|
||||||
|
|
||||||
def upgrade_config_to_db():
|
def upgrade_config_to_db():
|
||||||
logger.info(u"Tautulli Notifiers :: Upgrading to new notification system...")
|
logger.info(u"Tautulli Notifiers :: Upgrading to new notification system...")
|
||||||
|
|
||||||
|
@@ -144,14 +144,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 +311,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')
|
||||||
@@ -376,9 +377,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()
|
||||||
|
|
||||||
@@ -418,7 +429,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'):
|
||||||
@@ -432,7 +443,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')
|
||||||
@@ -461,12 +472,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),
|
||||||
@@ -483,7 +495,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,
|
||||||
@@ -633,7 +646,8 @@ class PlexTV(object):
|
|||||||
'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'),
|
||||||
'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)
|
||||||
|
|
||||||
@@ -641,10 +655,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)
|
||||||
@@ -737,3 +755,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,7 +533,12 @@ 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)
|
||||||
@@ -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')
|
||||||
|
|
||||||
@@ -1174,7 +1184,12 @@ class PmsConnect(object):
|
|||||||
if cache_key:
|
if cache_key:
|
||||||
metadata['_cache_time'] = int(time.time())
|
metadata['_cache_time'] = int(time.time())
|
||||||
|
|
||||||
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
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)
|
||||||
@@ -1386,7 +1401,7 @@ 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'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the transcode details
|
# Get the transcode details
|
||||||
@@ -1459,16 +1474,24 @@ class PmsConnect(object):
|
|||||||
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
|
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
|
||||||
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
|
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
|
||||||
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:
|
||||||
@@ -1661,7 +1684,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)
|
||||||
|
|
||||||
@@ -1735,49 +1758,55 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
# 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':
|
||||||
@@ -2156,8 +2185,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'),
|
||||||
@@ -2567,4 +2600,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
|
||||||
|
@@ -191,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',
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "beta"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.0.15-beta"
|
PLEXPY_RELEASE_VERSION = "v2.0.19-beta"
|
||||||
|
@@ -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:
|
||||||
|
@@ -33,14 +33,33 @@ ws_reconnect = 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
|
threading.Thread(target=run).start()
|
||||||
threading.Thread(target=run).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()
|
||||||
|
|
||||||
@@ -55,7 +74,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,
|
||||||
@@ -72,34 +91,29 @@ def run():
|
|||||||
global ws_reconnect
|
global ws_reconnect
|
||||||
ws_reconnect = False
|
ws_reconnect = 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:
|
||||||
|
ws = 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:
|
||||||
@@ -109,20 +123,24 @@ def run():
|
|||||||
reconnects = 0
|
reconnects = 0
|
||||||
|
|
||||||
except websocket.WebSocketConnectionClosedException:
|
except websocket.WebSocketConnectionClosedException:
|
||||||
if reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
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)
|
ws = 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()
|
ws.shutdown()
|
||||||
@@ -131,8 +149,8 @@ def run():
|
|||||||
|
|
||||||
except (websocket.WebSocketException, Exception) as e:
|
except (websocket.WebSocketException, Exception) as e:
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
|
ws.shutdown()
|
||||||
plexpy.WS_CONNECTED = False
|
plexpy.WS_CONNECTED = False
|
||||||
ws_exception = True
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if we recieved a restart notification and close websocket connection cleanly
|
# Check if we recieved a restart notification and close websocket connection cleanly
|
||||||
@@ -143,13 +161,6 @@ def run():
|
|||||||
start_thread()
|
start_thread()
|
||||||
|
|
||||||
if not plexpy.WS_CONNECTED and not ws_reconnect:
|
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.")
|
||||||
|
@@ -220,7 +220,7 @@ class AuthController(object):
|
|||||||
|
|
||||||
# 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.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr'))
|
||||||
host = cherrypy.request.headers.get('Origin')
|
host = cherrypy.request.headers.get('Host', cherrypy.request.headers.get('Origin'))
|
||||||
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,
|
||||||
|
@@ -28,6 +28,7 @@ from mako.lookup import TemplateLookup
|
|||||||
from mako import exceptions
|
from mako import exceptions
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
|
import activity_pinger
|
||||||
import common
|
import common
|
||||||
import config
|
import config
|
||||||
import database
|
import database
|
||||||
@@ -62,7 +63,7 @@ def serve_template(templatename, **kwargs):
|
|||||||
|
|
||||||
http_root = plexpy.HTTP_ROOT
|
http_root = plexpy.HTTP_ROOT
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
server_name = plexpy.CONFIG.PMS_NAME
|
||||||
cache_param = '?' + plexpy.CURRENT_VERSION or common.VERSION_NUMBER
|
cache_param = '?' + (plexpy.CURRENT_VERSION or common.VERSION_NUMBER)
|
||||||
|
|
||||||
_session = get_session_info()
|
_session = get_session_info()
|
||||||
|
|
||||||
@@ -98,10 +99,11 @@ class WebInterface(object):
|
|||||||
config = {
|
config = {
|
||||||
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
"pms_ip": plexpy.CONFIG.PMS_IP,
|
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
|
||||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||||
|
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
|
||||||
|
"pms_ssl": plexpy.CONFIG.PMS_SSL,
|
||||||
|
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
|
||||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||||
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL
|
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,7 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi("get_server_list")
|
@addtoapi("get_server_list")
|
||||||
def discover(self, token=None, include_cloud=True, all_servers=False, **kwargs):
|
def discover(self, token=None, include_cloud=True, all_servers=True, **kwargs):
|
||||||
""" Get all your servers that are published to Plex.tv.
|
""" Get all your servers that are published to Plex.tv.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -148,7 +150,7 @@ class WebInterface(object):
|
|||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
include_cloud = not (include_cloud == 'false')
|
include_cloud = not (include_cloud == 'false')
|
||||||
all_servers = (all_servers == 'true')
|
all_servers = not (all_servers == 'false')
|
||||||
|
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
servers_list = plex_tv.discover(include_cloud=include_cloud,
|
servers_list = plex_tv.discover(include_cloud=include_cloud,
|
||||||
@@ -456,12 +458,17 @@ class WebInterface(object):
|
|||||||
logger.warn(u"Unable to retrieve data for get_library_sections.")
|
logger.warn(u"Unable to retrieve data for get_library_sections.")
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def refresh_libraries_list(self, **kwargs):
|
def refresh_libraries_list(self, **kwargs):
|
||||||
""" Refresh the libraries list on it's own thread. """
|
""" Manually refresh the libraries list. """
|
||||||
threading.Thread(target=libraries.refresh_libraries).start()
|
|
||||||
logger.info(u"Manual libraries list refresh requested.")
|
logger.info(u"Manual libraries list refresh requested.")
|
||||||
return True
|
result = libraries.refresh_libraries()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Libraries list refreshed.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Unable to refresh libraries list.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
@@ -512,8 +519,6 @@ class WebInterface(object):
|
|||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
custom_thumb (str): The URL for the custom library thumbnail
|
custom_thumb (str): The URL for the custom library thumbnail
|
||||||
do_notify (int): 0 or 1
|
|
||||||
do_notify_created (int): 0 or 1
|
|
||||||
keep_history (int): 0 or 1
|
keep_history (int): 0 or 1
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -624,13 +629,14 @@ class WebInterface(object):
|
|||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
section_type (str): "movie", "show", "artist", "photo"
|
section_type (str): "movie", "show", "artist", "photo"
|
||||||
order_column (str): "added_at", "title", "container", "bitrate", "video_codec",
|
order_column (str): "added_at", "sort_title", "container", "bitrate", "video_codec",
|
||||||
"video_resolution", "video_framerate", "audio_codec", "audio_channels",
|
"video_resolution", "video_framerate", "audio_codec", "audio_channels",
|
||||||
"file_size", "last_played", "play_count"
|
"file_size", "last_played", "play_count"
|
||||||
order_dir (str): "desc" or "asc"
|
order_dir (str): "desc" or "asc"
|
||||||
start (int): Row to start from, 0
|
start (int): Row to start from, 0
|
||||||
length (int): Number of items to return, 25
|
length (int): Number of items to return, 25
|
||||||
search (str): A string to search for, "Thrones"
|
search (str): A string to search for, "Thrones"
|
||||||
|
refresh (str): "true" to refresh the media info table
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
@@ -674,7 +680,7 @@ class WebInterface(object):
|
|||||||
if not kwargs.get('json_data'):
|
if not kwargs.get('json_data'):
|
||||||
# Alias 'title' to 'sort_title'
|
# Alias 'title' to 'sort_title'
|
||||||
if kwargs.get('order_column') == 'title':
|
if kwargs.get('order_column') == 'title':
|
||||||
kwargs['order_column'] == 'sort_title'
|
kwargs['order_column'] = 'sort_title'
|
||||||
|
|
||||||
# TODO: Find some one way to automatically get the columns
|
# TODO: Find some one way to automatically get the columns
|
||||||
dt_columns = [("added_at", True, False),
|
dt_columns = [("added_at", True, False),
|
||||||
@@ -954,7 +960,7 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def delete_datatable_media_info_cache(self, section_id, **kwargs):
|
def delete_media_info_cache(self, section_id, **kwargs):
|
||||||
""" Delete the media info table cache for a specific library.
|
""" Delete the media info table cache for a specific library.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -974,14 +980,14 @@ class WebInterface(object):
|
|||||||
if section_id not in section_ids:
|
if section_id not in section_ids:
|
||||||
if section_id:
|
if section_id:
|
||||||
library_data = libraries.Libraries()
|
library_data = libraries.Libraries()
|
||||||
delete_row = library_data.delete_datatable_media_info_cache(section_id=section_id)
|
delete_row = library_data.delete_media_info_cache(section_id=section_id)
|
||||||
|
|
||||||
if delete_row:
|
if delete_row:
|
||||||
return {'message': delete_row}
|
return {'message': delete_row}
|
||||||
else:
|
else:
|
||||||
return {'message': 'no data received'}
|
return {'message': 'no data received'}
|
||||||
else:
|
else:
|
||||||
return {'message': 'Cannot refresh library while getting file sizes.'}
|
return {'message': 'Cannot delete media info cache while getting file sizes.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@@ -1078,12 +1084,17 @@ class WebInterface(object):
|
|||||||
return user_list
|
return user_list
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def refresh_users_list(self, **kwargs):
|
def refresh_users_list(self, **kwargs):
|
||||||
""" Refresh the users list on it's own thread. """
|
""" Manually refresh the users list. """
|
||||||
threading.Thread(target=users.refresh_users).start()
|
|
||||||
logger.info(u"Manual users list refresh requested.")
|
logger.info(u"Manual users list refresh requested.")
|
||||||
return True
|
result = users.refresh_users()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Users list refreshed.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Unable to refresh users list.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
@@ -1130,9 +1141,8 @@ class WebInterface(object):
|
|||||||
Optional paramters:
|
Optional paramters:
|
||||||
friendly_name(str): The friendly name of the user
|
friendly_name(str): The friendly name of the user
|
||||||
custom_thumb (str): The URL for the custom user thumbnail
|
custom_thumb (str): The URL for the custom user thumbnail
|
||||||
do_notify (int): 0 or 1
|
|
||||||
do_notify_created (int): 0 or 1
|
|
||||||
keep_history (int): 0 or 1
|
keep_history (int): 0 or 1
|
||||||
|
allow_guest (int): 0 or 1
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
@@ -2201,9 +2211,8 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
def get_sync(self, machine_id=None, user_id=None, **kwargs):
|
def get_sync(self, machine_id=None, user_id=None, **kwargs):
|
||||||
|
if user_id == 'null':
|
||||||
if not machine_id:
|
user_id = None
|
||||||
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
|
||||||
|
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id)
|
result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id)
|
||||||
@@ -2566,8 +2575,8 @@ class WebInterface(object):
|
|||||||
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
|
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
|
||||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
"pms_ssl": plexpy.CONFIG.PMS_SSL,
|
||||||
"pms_is_remote": checked(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_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
|
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
|
||||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||||
@@ -2612,7 +2621,8 @@ class WebInterface(object):
|
|||||||
"tv_watched_percent": plexpy.CONFIG.TV_WATCHED_PERCENT,
|
"tv_watched_percent": plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
"music_watched_percent": plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
"music_watched_percent": plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
"themoviedb_lookup": checked(plexpy.CONFIG.THEMOVIEDB_LOOKUP),
|
"themoviedb_lookup": checked(plexpy.CONFIG.THEMOVIEDB_LOOKUP),
|
||||||
"tvmaze_lookup": checked(plexpy.CONFIG.TVMAZE_LOOKUP)
|
"tvmaze_lookup": checked(plexpy.CONFIG.TVMAZE_LOOKUP),
|
||||||
|
"show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS
|
||||||
}
|
}
|
||||||
|
|
||||||
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
|
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
|
||||||
@@ -2626,7 +2636,7 @@ class WebInterface(object):
|
|||||||
checked_configs = [
|
checked_configs = [
|
||||||
"launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
|
"launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
|
||||||
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
|
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
|
||||||
"pms_ssl", "pms_is_remote", "pms_url_manual", "week_start_monday",
|
"pms_url_manual", "week_start_monday",
|
||||||
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
||||||
"notify_consecutive", "notify_upload_posters", "notify_recently_added_upgrade",
|
"notify_consecutive", "notify_upload_posters", "notify_recently_added_upgrade",
|
||||||
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
||||||
@@ -2749,7 +2759,7 @@ class WebInterface(object):
|
|||||||
|
|
||||||
# If first run, start websocket
|
# If first run, start websocket
|
||||||
if first_run:
|
if first_run:
|
||||||
web_socket.start_thread()
|
activity_pinger.connect_server(log=True, startup=True)
|
||||||
|
|
||||||
# Reconfigure scheduler if intervals changed
|
# Reconfigure scheduler if intervals changed
|
||||||
if reschedule:
|
if reschedule:
|
||||||
@@ -2798,12 +2808,16 @@ class WebInterface(object):
|
|||||||
def get_server_update_params(self, **kwargs):
|
def get_server_update_params(self, **kwargs):
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
plexpass = plex_tv.get_plexpass_status()
|
plexpass = plex_tv.get_plexpass_status()
|
||||||
|
|
||||||
|
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
||||||
|
|
||||||
return {'plexpass': plexpass,
|
return {'plexpass': plexpass,
|
||||||
'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get(
|
'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get(
|
||||||
plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM),
|
plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM),
|
||||||
'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL,
|
'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL,
|
||||||
'pms_update_distro': plexpy.CONFIG.PMS_UPDATE_DISTRO,
|
'pms_update_distro': plexpy.CONFIG.PMS_UPDATE_DISTRO,
|
||||||
'pms_update_distro_build': plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD}
|
'pms_update_distro_build': plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD,
|
||||||
|
'plex_update_channel': 'plexpass' if update_channel == 'beta' else 'public'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@@ -3011,12 +3025,14 @@ class WebInterface(object):
|
|||||||
Pass all the config options for the agent with the agent prefix:
|
Pass all the config options for the agent with the agent prefix:
|
||||||
e.g. For Telegram: telegram_bot_token
|
e.g. For Telegram: telegram_bot_token
|
||||||
telegram_chat_id
|
telegram_chat_id
|
||||||
disable_web_preview
|
telegram_disable_web_preview
|
||||||
html_support
|
telegram_html_support
|
||||||
incl_poster
|
telegram_incl_poster
|
||||||
incl_subject
|
telegram_incl_subject
|
||||||
Notify actions with 'trigger_' prefix (trigger_on_play, trigger_on_stop, etc.),
|
Notify actions (int): 0 or 1,
|
||||||
and notify text with 'text_' prefix (text_on_play_subject, text_on_play_body, etc.) are optional.
|
e.g. on_play, on_stop, etc.
|
||||||
|
Notify text (str):
|
||||||
|
e.g. on_play_subject, on_play_body, etc.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
@@ -3197,6 +3213,16 @@ class WebInterface(object):
|
|||||||
logger.warn(msg)
|
logger.warn(msg)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
def zapier_test_hook(self, zapier_hook='', **kwargs):
|
||||||
|
success = notifiers.ZAPIER(config={'hook': zapier_hook})._test_hook()
|
||||||
|
if success:
|
||||||
|
return {'result': 'success', 'msg': 'Test Zapier webhook sent.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'msg': 'Failed to send test Zapier webhook.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def set_notification_config(self, **kwargs):
|
def set_notification_config(self, **kwargs):
|
||||||
@@ -3492,10 +3518,53 @@ class WebInterface(object):
|
|||||||
return apikey
|
return apikey
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def checkGithub(self, **kwargs):
|
@addtoapi()
|
||||||
|
def update_check(self, **kwargs):
|
||||||
|
""" Check for Tautulli updates.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json
|
||||||
|
{"result": "success",
|
||||||
|
"update": true,
|
||||||
|
"message": "An update for Tautulli is available."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
versioncheck.checkGithub()
|
versioncheck.checkGithub()
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
|
||||||
|
if not plexpy.CURRENT_VERSION:
|
||||||
|
return {'result': 'error',
|
||||||
|
'message': 'You are running an unknown version of Tautulli.',
|
||||||
|
'update': None}
|
||||||
|
|
||||||
|
elif plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and \
|
||||||
|
plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
||||||
|
return {'result': 'success',
|
||||||
|
'update': True,
|
||||||
|
'message': 'An update for Tautulli is available.',
|
||||||
|
'latest_version': plexpy.LATEST_VERSION,
|
||||||
|
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||||
|
'compare_url': helpers.anon_url(
|
||||||
|
'https://github.com/%s/%s/compare/%s...%s'
|
||||||
|
% (plexpy.CONFIG.GIT_USER,
|
||||||
|
plexpy.CONFIG.GIT_REPO,
|
||||||
|
plexpy.CURRENT_VERSION,
|
||||||
|
plexpy.LATEST_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {'result': 'success',
|
||||||
|
'update': False,
|
||||||
|
'message': 'Tautulli is up to date.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@@ -3578,6 +3647,8 @@ class WebInterface(object):
|
|||||||
if metadata:
|
if metadata:
|
||||||
poster_info = data_factory.get_poster_info(metadata=metadata)
|
poster_info = data_factory.get_poster_info(metadata=metadata)
|
||||||
metadata.update(poster_info)
|
metadata.update(poster_info)
|
||||||
|
lookup_info = data_factory.get_lookup_info(metadata=metadata)
|
||||||
|
metadata.update(lookup_info)
|
||||||
else:
|
else:
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
|
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
|
||||||
@@ -3585,6 +3656,8 @@ class WebInterface(object):
|
|||||||
data_factory = datafactory.DataFactory()
|
data_factory = datafactory.DataFactory()
|
||||||
poster_info = data_factory.get_poster_info(metadata=metadata)
|
poster_info = data_factory.get_poster_info(metadata=metadata)
|
||||||
metadata.update(poster_info)
|
metadata.update(poster_info)
|
||||||
|
lookup_info = data_factory.get_lookup_info(metadata=metadata)
|
||||||
|
metadata.update(lookup_info)
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
if metadata['section_id'] and not allow_session_library(metadata['section_id']):
|
if metadata['section_id'] and not allow_session_library(metadata['section_id']):
|
||||||
@@ -3869,15 +3942,60 @@ class WebInterface(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def delete_poster_url(self, rating_key='', **kwargs):
|
@addtoapi()
|
||||||
|
def delete_imgur_poster(self, rating_key='', **kwargs):
|
||||||
|
""" Delete the Imgur poster.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
rating_key (int): 1234
|
||||||
|
(Note: Must be the movie, show, season, artist, or album rating key)
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json:
|
||||||
|
{"result": "success",
|
||||||
|
"message": "Deleted Imgur poster."}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
data_factory = datafactory.DataFactory()
|
data_factory = datafactory.DataFactory()
|
||||||
result = data_factory.delete_poster_url(rating_key=rating_key)
|
result = data_factory.delete_poster_url(rating_key=rating_key)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return {'result': 'success', 'message': 'Deleted Imgur poster url.'}
|
return {'result': 'success', 'message': 'Deleted Imgur poster.'}
|
||||||
else:
|
else:
|
||||||
return {'result': 'error', 'message': 'Failed to delete Imgur poster url.'}
|
return {'result': 'error', 'message': 'Failed to delete Imgur poster.'}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def delete_lookup_info(self, rating_key='', title='', **kwargs):
|
||||||
|
""" Delete the 3rd party API lookup info.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
rating_key (int): 1234
|
||||||
|
(Note: Must be the movie, show, or artist rating key)
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json:
|
||||||
|
{"result": "success",
|
||||||
|
"message": "Deleted lookup info."}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_factory = datafactory.DataFactory()
|
||||||
|
result = data_factory.delete_lookup_info(rating_key=rating_key, title=title)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Deleted lookup info.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Failed to delete lookup info.'}
|
||||||
|
|
||||||
|
|
||||||
##### Search #####
|
##### Search #####
|
||||||
@@ -4103,11 +4221,15 @@ class WebInterface(object):
|
|||||||
],
|
],
|
||||||
"added_at": "1461572396",
|
"added_at": "1461572396",
|
||||||
"art": "/library/metadata/1219/art/1462175063",
|
"art": "/library/metadata/1219/art/1462175063",
|
||||||
|
"audience_rating": "8",
|
||||||
|
"banner": "/library/metadata/1219/banner/1462175063",
|
||||||
|
"collections": [],
|
||||||
"content_rating": "TV-MA",
|
"content_rating": "TV-MA",
|
||||||
"directors": [
|
"directors": [
|
||||||
"Jeremy Podeswa"
|
"Jeremy Podeswa"
|
||||||
],
|
],
|
||||||
"duration": "2998290",
|
"duration": "2998290",
|
||||||
|
"full_title": "Game of Thrones - The Red Woman",
|
||||||
"genres": [
|
"genres": [
|
||||||
"Adventure",
|
"Adventure",
|
||||||
"Drama",
|
"Drama",
|
||||||
@@ -4121,6 +4243,74 @@ class WebInterface(object):
|
|||||||
"last_viewed_at": "1462165717",
|
"last_viewed_at": "1462165717",
|
||||||
"library_name": "TV Shows",
|
"library_name": "TV Shows",
|
||||||
"media_index": "1",
|
"media_index": "1",
|
||||||
|
"media_info": [
|
||||||
|
{
|
||||||
|
"aspect_ratio": "1.78",
|
||||||
|
"audio_channel_layout": "5.1",
|
||||||
|
"audio_channels": "6",
|
||||||
|
"audio_codec": "ac3",
|
||||||
|
"audio_profile": "",
|
||||||
|
"bitrate": "10617",
|
||||||
|
"container": "mkv",
|
||||||
|
"height": "1078",
|
||||||
|
"id": "257925",
|
||||||
|
"optimized_version": 0,
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"file": "/media/TV Shows/Game of Thrones/Season 06/Game of Thrones - S06E01 - The Red Woman.mkv",
|
||||||
|
"file_size": "3979115377",
|
||||||
|
"id": "274169",
|
||||||
|
"indexes": 1,
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"id": "511663",
|
||||||
|
"type": "1",
|
||||||
|
"video_bit_depth": "8",
|
||||||
|
"video_bitrate": "10233",
|
||||||
|
"video_codec": "h264",
|
||||||
|
"video_codec_level": "41",
|
||||||
|
"video_frame_rate": "23.976",
|
||||||
|
"video_height": "1078",
|
||||||
|
"video_language": "",
|
||||||
|
"video_language_code": "",
|
||||||
|
"video_profile": "high",
|
||||||
|
"video_ref_frames": "4",
|
||||||
|
"video_width": "1920"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"audio_bitrate": "384",
|
||||||
|
"audio_bitrate_mode": "",
|
||||||
|
"audio_channel_layout": "5.1(side)",
|
||||||
|
"audio_channels": "6",
|
||||||
|
"audio_codec": "ac3",
|
||||||
|
"audio_language": "",
|
||||||
|
"audio_language_code": "",
|
||||||
|
"audio_profile": "",
|
||||||
|
"audio_sample_rate": "48000",
|
||||||
|
"id": "511664",
|
||||||
|
"type": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "511953",
|
||||||
|
"subtitle_codec": "srt",
|
||||||
|
"subtitle_container": "",
|
||||||
|
"subtitle_forced": 0,
|
||||||
|
"subtitle_format": "srt",
|
||||||
|
"subtitle_language": "English",
|
||||||
|
"subtitle_language_code": "eng",
|
||||||
|
"subtitle_location": "external",
|
||||||
|
"type": "3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"video_codec": "h264",
|
||||||
|
"video_framerate": "24p",
|
||||||
|
"video_profile": "high",
|
||||||
|
"video_resolution": "1080",
|
||||||
|
"width": "1920"
|
||||||
|
}
|
||||||
|
],
|
||||||
"media_type": "episode",
|
"media_type": "episode",
|
||||||
"originally_available_at": "2016-04-24",
|
"originally_available_at": "2016-04-24",
|
||||||
"parent_media_index": "6",
|
"parent_media_index": "6",
|
||||||
@@ -4130,11 +4320,13 @@ class WebInterface(object):
|
|||||||
"rating": "7.8",
|
"rating": "7.8",
|
||||||
"rating_key": "153037",
|
"rating_key": "153037",
|
||||||
"section_id": "2",
|
"section_id": "2",
|
||||||
|
"sort_title": "Game of Thrones",
|
||||||
"studio": "HBO",
|
"studio": "HBO",
|
||||||
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
|
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
|
||||||
"tagline": "",
|
"tagline": "",
|
||||||
"thumb": "/library/metadata/153037/thumb/1462175060",
|
"thumb": "/library/metadata/153037/thumb/1462175060",
|
||||||
"title": "The Red Woman",
|
"title": "The Red Woman",
|
||||||
|
"user_rating": "9.0",
|
||||||
"updated_at": "1462175060",
|
"updated_at": "1462175060",
|
||||||
"writers": [
|
"writers": [
|
||||||
"David Benioff",
|
"David Benioff",
|
||||||
@@ -4373,67 +4565,219 @@ class WebInterface(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
{"stream_count": 3,
|
{"lan_bandwidth": 25318,
|
||||||
"sessions":
|
"sessions": [
|
||||||
[{"art": "/library/metadata/1219/art/1462175063",
|
{
|
||||||
"aspect_ratio": "1.78",
|
"actors": [
|
||||||
"audio_channels": "6",
|
"Kit Harington",
|
||||||
"audio_codec": "ac3",
|
"Emilia Clarke",
|
||||||
"audio_decision": "transcode",
|
"Isaac Hempstead-Wright",
|
||||||
"bif_thumb": "/library/parts/274169/indexes/sd/",
|
"Maisie Williams",
|
||||||
"bitrate": "10617",
|
"Liam Cunningham",
|
||||||
"container": "mkv",
|
],
|
||||||
"content_rating": "TV-MA",
|
"added_at": "1461572396",
|
||||||
"duration": "2998290",
|
"allow_guest": 1,
|
||||||
"friendly_name": "Mother of Dragons",
|
"art": "/library/metadata/1219/art/1503306930",
|
||||||
"grandparent_rating_key": "1219",
|
"aspect_ratio": "1.78",
|
||||||
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
|
"audience_rating": "",
|
||||||
"grandparent_title": "Game of Thrones",
|
"audio_bitrate": "384",
|
||||||
"height": "1078",
|
"audio_bitrate_mode": "",
|
||||||
"indexes": 1,
|
"audio_channel_layout": "5.1(side)",
|
||||||
"ip_address": "xxx.xxx.xxx.xxx",
|
"audio_channels": "6",
|
||||||
"labels": [],
|
"audio_codec": "ac3",
|
||||||
"machine_id": "83f189w617623ccs6a1lqpby",
|
"audio_decision": "direct play",
|
||||||
"media_index": "1",
|
"audio_language": "",
|
||||||
"media_type": "episode",
|
"audio_language_code": "",
|
||||||
"parent_media_index": "6",
|
"audio_profile": "",
|
||||||
"parent_rating_key": "153036",
|
"audio_sample_rate": "48000",
|
||||||
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
|
"bandwidth": "25318",
|
||||||
"parent_title": "",
|
"banner": "/library/metadata/1219/banner/1503306930",
|
||||||
"platform": "Chrome",
|
"bif_thumb": "/library/parts/274169/indexes/sd/1000",
|
||||||
"player": "Plex Web (Chrome)",
|
"bitrate": "10617",
|
||||||
"progress_percent": "0",
|
"channel_stream": 0,
|
||||||
"rating_key": "153037",
|
"collections": [],
|
||||||
"section_id": "2",
|
"container": "mkv",
|
||||||
"session_key": "291",
|
"content_rating": "TV-MA",
|
||||||
"state": "playing",
|
"deleted_user": 0,
|
||||||
"throttled": "1",
|
"device": "Windows",
|
||||||
"thumb": "/library/metadata/153037/thumb/1462175060",
|
"directors": [
|
||||||
"title": "The Red Woman",
|
"Jeremy Podeswa"
|
||||||
"transcode_audio_channels": "2",
|
],
|
||||||
"transcode_audio_codec": "aac",
|
"do_notify": 0,
|
||||||
"transcode_container": "mkv",
|
"duration": "2998272",
|
||||||
"transcode_height": "1078",
|
"email": "Jon.Snow.1337@CastleBlack.com",
|
||||||
"transcode_key": "tiv5p524wcupe8nxegc26s9k9",
|
"file": "/media/TV Shows/Game of Thrones/Season 06/Game of Thrones - S06E01 - The Red Woman.mkv",
|
||||||
"transcode_progress": 2,
|
"file_size": "3979115377",
|
||||||
"transcode_protocol": "http",
|
"friendly_name": "Jon Snow",
|
||||||
"transcode_speed": "0.0",
|
"full_title": "Game of Thrones - The Red Woman",
|
||||||
"transcode_video_codec": "h264",
|
"genres": [
|
||||||
"transcode_width": "1920",
|
"Adventure",
|
||||||
"user": "DanyKhaleesi69",
|
"Drama",
|
||||||
"user_id": 8008135,
|
"Fantasy"
|
||||||
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar",
|
],
|
||||||
"video_codec": "h264",
|
"grandparent_rating_key": "1219",
|
||||||
"video_decision": "copy",
|
"grandparent_thumb": "/library/metadata/1219/thumb/1503306930",
|
||||||
"video_framerate": "24p",
|
"grandparent_title": "Game of Thrones",
|
||||||
"video_resolution": "1080",
|
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||||
"view_offset": "",
|
"height": "1078",
|
||||||
"width": "1920",
|
"id": "",
|
||||||
"year": "2016"
|
"indexes": 1,
|
||||||
},
|
"ip_address": "10.10.10.1",
|
||||||
{...},
|
"ip_address_public": "64.123.23.111",
|
||||||
{...}
|
"is_admin": 1,
|
||||||
]
|
"is_allow_sync": null,
|
||||||
|
"is_home_user": 1,
|
||||||
|
"is_restricted": 0,
|
||||||
|
"keep_history": 1,
|
||||||
|
"labels": [],
|
||||||
|
"last_viewed_at": "1462165717",
|
||||||
|
"library_name": "TV Shows",
|
||||||
|
"local": "1",
|
||||||
|
"location": "lan",
|
||||||
|
"machine_id": "lmd93nkn12k29j2lnm",
|
||||||
|
"media_index": "1",
|
||||||
|
"media_type": "episode",
|
||||||
|
"optimized_version": 0,
|
||||||
|
"optimized_version_profile": "",
|
||||||
|
"optimized_version_title": "",
|
||||||
|
"originally_available_at": "2016-04-24",
|
||||||
|
"parent_media_index": "6",
|
||||||
|
"parent_rating_key": "153036",
|
||||||
|
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
|
||||||
|
"parent_title": "Season 6",
|
||||||
|
"platform": "Plex Media Player",
|
||||||
|
"platform_name": "plex",
|
||||||
|
"platform_version": "2.4.1.787-54a020cd",
|
||||||
|
"player": "Castle-PC",
|
||||||
|
"product": "Plex Media Player",
|
||||||
|
"product_version": "3.35.2",
|
||||||
|
"profile": "Konvergo",
|
||||||
|
"progress_percent": "0",
|
||||||
|
"quality_profile": "Original",
|
||||||
|
"rating": "7.8",
|
||||||
|
"rating_key": "153037",
|
||||||
|
"section_id": "2",
|
||||||
|
"session_id": "helf15l3rxgw01xxe0jf3l3d",
|
||||||
|
"session_key": "27",
|
||||||
|
"shared_libraries": [
|
||||||
|
"10",
|
||||||
|
"1",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"15",
|
||||||
|
"20",
|
||||||
|
"2"
|
||||||
|
],
|
||||||
|
"sort_title": "Red Woman",
|
||||||
|
"state": "playing",
|
||||||
|
"stream_aspect_ratio": "1.78",
|
||||||
|
"stream_audio_bitrate": "384",
|
||||||
|
"stream_audio_bitrate_mode": "",
|
||||||
|
"stream_audio_channel_layout": "5.1(side)",
|
||||||
|
"stream_audio_channel_layout_": "5.1(side)",
|
||||||
|
"stream_audio_channels": "6",
|
||||||
|
"stream_audio_codec": "ac3",
|
||||||
|
"stream_audio_decision": "direct play",
|
||||||
|
"stream_audio_language": "",
|
||||||
|
"stream_audio_language_code": "",
|
||||||
|
"stream_audio_sample_rate": "48000",
|
||||||
|
"stream_bitrate": "10617",
|
||||||
|
"stream_container": "mkv",
|
||||||
|
"stream_container_decision": "direct play",
|
||||||
|
"stream_duration": "2998272",
|
||||||
|
"stream_subtitle_codec": "",
|
||||||
|
"stream_subtitle_container": "",
|
||||||
|
"stream_subtitle_decision": "",
|
||||||
|
"stream_subtitle_forced": 0,
|
||||||
|
"stream_subtitle_format": "",
|
||||||
|
"stream_subtitle_language": "",
|
||||||
|
"stream_subtitle_language_code": "",
|
||||||
|
"stream_subtitle_location": "",
|
||||||
|
"stream_video_bit_depth": "8",
|
||||||
|
"stream_video_bitrate": "10233",
|
||||||
|
"stream_video_codec": "h264",
|
||||||
|
"stream_video_codec_level": "41",
|
||||||
|
"stream_video_decision": "direct play",
|
||||||
|
"stream_video_framerate": "24p",
|
||||||
|
"stream_video_height": "1078",
|
||||||
|
"stream_video_language": "",
|
||||||
|
"stream_video_language_code": "",
|
||||||
|
"stream_video_ref_frames": "4",
|
||||||
|
"stream_video_resolution": "1080",
|
||||||
|
"stream_video_width": "1920",
|
||||||
|
"studio": "HBO",
|
||||||
|
"subtitle_codec": "",
|
||||||
|
"subtitle_container": "",
|
||||||
|
"subtitle_decision": "",
|
||||||
|
"subtitle_forced": 0,
|
||||||
|
"subtitle_format": "",
|
||||||
|
"subtitle_language": "",
|
||||||
|
"subtitle_language_code": "",
|
||||||
|
"subtitle_location": "",
|
||||||
|
"subtitles": 0,
|
||||||
|
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
|
||||||
|
"synced_version": 0,
|
||||||
|
"synced_version_profile": "",
|
||||||
|
"tagline": "",
|
||||||
|
"throttled": "0",
|
||||||
|
"thumb": "/library/metadata/153037/thumb/1503889207",
|
||||||
|
"title": "The Red Woman",
|
||||||
|
"transcode_audio_channels": "",
|
||||||
|
"transcode_audio_codec": "",
|
||||||
|
"transcode_container": "",
|
||||||
|
"transcode_decision": "direct play",
|
||||||
|
"transcode_height": "",
|
||||||
|
"transcode_hw_decode": "",
|
||||||
|
"transcode_hw_decode_title": "",
|
||||||
|
"transcode_hw_decoding": 0,
|
||||||
|
"transcode_hw_encode": "",
|
||||||
|
"transcode_hw_encode_title": "",
|
||||||
|
"transcode_hw_encoding": 0,
|
||||||
|
"transcode_hw_full_pipeline": 0,
|
||||||
|
"transcode_hw_requested": 0,
|
||||||
|
"transcode_key": "",
|
||||||
|
"transcode_progress": 0,
|
||||||
|
"transcode_protocol": "",
|
||||||
|
"transcode_speed": "",
|
||||||
|
"transcode_throttled": 0,
|
||||||
|
"transcode_video_codec": "",
|
||||||
|
"transcode_width": "",
|
||||||
|
"type": "",
|
||||||
|
"updated_at": "1503889207",
|
||||||
|
"user": "LordCommanderSnow",
|
||||||
|
"user_id": 133788,
|
||||||
|
"user_rating": "",
|
||||||
|
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
|
||||||
|
"username": "LordCommanderSnow",
|
||||||
|
"video_bit_depth": "8",
|
||||||
|
"video_bitrate": "10233",
|
||||||
|
"video_codec": "h264",
|
||||||
|
"video_codec_level": "41",
|
||||||
|
"video_decision": "direct play",
|
||||||
|
"video_frame_rate": "23.976",
|
||||||
|
"video_framerate": "24p",
|
||||||
|
"video_height": "1078",
|
||||||
|
"video_language": "",
|
||||||
|
"video_language_code": "",
|
||||||
|
"video_profile": "high",
|
||||||
|
"video_ref_frames": "4",
|
||||||
|
"video_resolution": "1080",
|
||||||
|
"video_width": "1920",
|
||||||
|
"view_offset": "1000",
|
||||||
|
"width": "1920",
|
||||||
|
"writers": [
|
||||||
|
"David Benioff",
|
||||||
|
"D. B. Weiss"
|
||||||
|
],
|
||||||
|
"year": "2016"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stream_count": "1",
|
||||||
|
"stream_count_direct_play": 1,
|
||||||
|
"stream_count_direct_stream": 0,
|
||||||
|
"stream_count_transcode": 0,
|
||||||
|
"total_bandwidth": 25318,
|
||||||
|
"wan_bandwidth": 0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
@@ -4570,27 +4914,29 @@ class WebInterface(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
[{"content_type": "video",
|
[{"audio_bitrate": "192",
|
||||||
|
"client_id": "95434se643fsf24f-com-plexapp-android",
|
||||||
|
"content_type": "video",
|
||||||
"device_name": "Tyrion's iPad",
|
"device_name": "Tyrion's iPad",
|
||||||
"failure": "",
|
"failure": "",
|
||||||
"friendly_name": "Tyrion Lannister",
|
"item_complete_count": "1",
|
||||||
"item_complete_count": "0",
|
|
||||||
"item_count": "1",
|
"item_count": "1",
|
||||||
"item_downloaded_count": "0",
|
"item_downloaded_count": "1",
|
||||||
"item_downloaded_percent_complete": 0,
|
"item_downloaded_percent_complete": 100,
|
||||||
"metadata_type": "movie",
|
"metadata_type": "movie",
|
||||||
"music_bitrate": "192",
|
|
||||||
"photo_quality": "74",
|
"photo_quality": "74",
|
||||||
"platform": "iOS",
|
"platform": "iOS",
|
||||||
"rating_key": "154092",
|
"rating_key": "154092",
|
||||||
"root_title": "Deadpool",
|
"root_title": "Movies",
|
||||||
"state": "pending",
|
"state": "complete",
|
||||||
"sync_id": "11617019",
|
"sync_id": "11617019",
|
||||||
"title": "Deadpool",
|
"sync_title": "Deadpool",
|
||||||
"total_size": "0",
|
"total_size": "560718134",
|
||||||
|
"user": "DrukenDwarfMan",
|
||||||
"user_id": "696969",
|
"user_id": "696969",
|
||||||
"username": "DrukenDwarfMan",
|
"username": "DrukenDwarfMan",
|
||||||
"video_quality": "60"
|
"video_bitrate": "4000"
|
||||||
|
"video_quality": "100"
|
||||||
},
|
},
|
||||||
{...},
|
{...},
|
||||||
{...}
|
{...}
|
||||||
|
Reference in New Issue
Block a user