Compare commits

...

71 Commits

Author SHA1 Message Date
JonnyWong16
066a95d209 v2.0.19-beta 2018-02-16 22:22:51 -08:00
JonnyWong16
c7cc476623 Change "Close" to "Dismiss" in update bar 2018-02-16 15:24:48 -08:00
JonnyWong16
bd44eb7fe4 Redraw table after refresh 2018-02-16 11:22:39 -08:00
JonnyWong16
6ec4f51077 Don't delete session cache folder on startup 2018-02-16 11:18:56 -08:00
JonnyWong16
b4a4f60b04 Fix manual refreshing the libraries/users list 2018-02-16 11:17:30 -08:00
JonnyWong16
dc4e6edc9a Fix update bar dismiss if it was not shown originally 2018-02-16 11:00:01 -08:00
JonnyWong16
60b362b19e Transparent update bar 2018-02-16 10:58:00 -08:00
JonnyWong16
7e81ce8c06 Fade in/out update message 2018-02-16 10:47:59 -08:00
JonnyWong16
c7f9e2f721 Change update bar css 2018-02-16 10:31:22 -08:00
JonnyWong16
cab8b1c041 Check for updates without refreshing the page 2018-02-16 10:24:55 -08:00
JonnyWong16
16f270691d Check on watched notification states before adding to the queue 2018-02-15 15:16:50 -08:00
JonnyWong16
d94a1efe75 Add media info table refresh to API docs 2018-02-15 15:15:36 -08:00
JonnyWong16
12755970b7 Fix failure to make session cache folder on startup 2018-02-15 12:21:44 -08:00
JonnyWong16
93e4853ea2 Fix delete media info cache 2018-02-14 11:19:53 -08:00
JonnyWong16
5e0c0365fb Change button colours on setup wizard 2018-02-14 09:48:04 -08:00
JonnyWong16
c2713c53dd Only connect if first run is complete 2018-02-14 08:53:49 -08:00
JonnyWong16
90443b4028 Catch failed to retrieve Plex Cloud status 2018-02-14 08:53:27 -08:00
JonnyWong16
e0109ed179 Combine connection function for cloud and non-cloud servers 2018-02-14 08:45:45 -08:00
JonnyWong16
a53afe05a2 Check cloud status on startup before connecting websocket 2018-02-14 06:55:44 -08:00
JonnyWong16
a5d2467bfe Less log spam of cloud server status 2018-02-14 06:39:42 -08:00
JonnyWong16
8447663e27 Fix server up/down status on Tautulli startup 2018-02-14 06:35:59 -08:00
JonnyWong16
64d67d8209 Hide remote access check message 2018-02-13 22:48:28 -08:00
JonnyWong16
78034b82a9 Send Use SSL and Remote Server checkbox values when disabled 2018-02-13 22:06:09 -08:00
JonnyWong16
f77bd6c17b Move server selectize dropdown container 2018-02-13 19:30:20 -08:00
JonnyWong16
2621da7d36 Add server selection dropdown to settings 2018-02-13 19:22:11 -08:00
JonnyWong16
e1dca1509a Reconnect Plex Cloud without keeping the server awake 2018-02-13 10:49:11 -08:00
JonnyWong16
df016243dd Refactor some websocket connection code 2018-02-13 08:48:54 -08:00
JonnyWong16
be72693fec Catch WebSocketException when attempting to reconnect 2018-02-13 07:08:35 -08:00
JonnyWong16
33a1ebdb1a Show location for masked session info 2018-02-12 17:40:11 -08:00
JonnyWong16
030f9d334b Improve server selectize on setup wizard 2018-02-12 17:33:35 -08:00
JonnyWong16
dc743ac378 Fix show full changelog on fresh install 2018-02-12 17:16:43 -08:00
JonnyWong16
0010cbe21f Update masked info for guest access 2018-02-12 11:35:34 -08:00
JonnyWong16
3a5d5918de v2.0.18-beta 2018-02-12 09:44:57 -08:00
JonnyWong16
3380e39de2 Add button to delete 3rd party API lookup info 2018-02-12 09:31:44 -08:00
JonnyWong16
7d31079897 Change group history table on by default 2018-02-12 08:20:58 -08:00
JonnyWong16
c287b6df77 Fix DepreciationWarning error for URIs with query string parameters 2018-02-12 08:15:21 -08:00
JonnyWong16
dab1f8ba20 Save The Movie Database info after lookup 2018-02-11 22:03:15 -08:00
JonnyWong16
a26de7f6c2 Move repository 2018-02-11 20:23:37 -08:00
JonnyWong16
ab32b2cbc2 Compressed screenshot for readme 2018-02-11 19:15:20 -08:00
JonnyWong16
503c249fc3 Update API docs 2018-02-11 19:11:31 -08:00
JonnyWong16
2a03ce757e Add toggle for advanced settings 2018-02-11 17:55:37 -08:00
JonnyWong16
373a15524f Remove redundant settings headers 2018-02-11 17:17:08 -08:00
JonnyWong16
13036183d3 Move extra settings to other tabs 2018-02-11 16:38:21 -08:00
JonnyWong16
170591c79e Move some settings, split notifications agents back out 2018-02-11 16:11:00 -08:00
JonnyWong16
a15d225a5f Fix missing Host in login logs for Firefox 2018-02-10 12:23:05 -08:00
JonnyWong16
a0106874e2 Fix paused and resume notifications only triggering once 2018-02-08 21:19:43 -08:00
JonnyWong16
ab157d1c0e Fix default text on Tautulli update notification 2018-02-08 12:21:21 -08:00
JonnyWong16
0b95c9fe2e Add Imgur poster deletion 2018-02-07 17:59:08 -08:00
JonnyWong16
d693514ca9 Notification exclusion tags change media item to media type 2018-02-07 11:44:16 -08:00
JonnyWong16
56987b3aaa Add note to notification exclusion tags 2018-02-07 11:42:17 -08:00
JonnyWong16
3ca1bd5d78 Change custom conditions negative operators to "and" 2018-02-03 17:02:13 -08:00
JonnyWong16
5d2219f2f8 v2.0.17-beta 2018-02-03 09:35:05 -08:00
JonnyWong16
56dc28eed3 Clear session metadata cache on startup 2018-02-03 09:06:05 -08:00
JonnyWong16
3e723d4373 Fix photo album media type 2018-02-02 23:49:38 -08:00
JonnyWong16
f5e341e655 Don't sanitize tags for Slack and Discord 2018-02-02 23:22:41 -08:00
JonnyWong16
3c81100957 Fix media info table sorting 2018-02-02 23:03:48 -08:00
JonnyWong16
304378f93b Add Zapier notification agent 2018-02-01 22:11:33 -08:00
JonnyWong16
de6b6e8124 Check for any falsy value in sync item filters 2018-01-31 08:59:37 -08:00
JonnyWong16
d15223fb1a v2.0.16-beta 2018-01-30 23:20:34 -08:00
JonnyWong16
d29a12b6db Add user filter to the synced table 2018-01-30 23:07:21 -08:00
JonnyWong16
9100e25a21 Pass copy of notification data to prevent multithreading issues 2018-01-30 23:04:44 -08:00
JonnyWong16
7672f1955e Fix sync table not loading 2018-01-30 21:19:37 -08:00
JonnyWong16
5f52171fc4 Add "Use Server Setting" as Plex update channel 2018-01-30 19:56:48 -08:00
JonnyWong16
31ac82ad71 Comment out logging for writing session history to database 2018-01-30 19:06:10 -08:00
JonnyWong16
38ca4e37a6 Fix matching of synced playback 2018-01-30 19:04:30 -08:00
JonnyWong16
3c55550702 Add logging for writing session history to database 2018-01-30 10:04:28 -08:00
JonnyWong16
7dff6b121b Log force stopped message 2018-01-30 09:31:13 -08:00
JonnyWong16
d77d889695 Fix activity callback function argument 2018-01-30 09:13:06 -08:00
JonnyWong16
318a21438f Fix sometimes time showing as "0:60" 2018-01-28 20:19:49 -08:00
JonnyWong16
7175b57a28 Fix "unknown" stream resolution in graphs 2018-01-28 10:06:47 -08:00
JonnyWong16
770f12b632 Hide advanced settings 2018-01-20 14:03:23 -08:00
44 changed files with 2835 additions and 1289 deletions

841
API.md

File diff suppressed because it is too large Load Diff

View File

@@ -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:

View File

@@ -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

View File

@@ -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.
--> -->

View File

@@ -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

View File

@@ -2,7 +2,7 @@
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/tQcWEUp) [![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/tQcWEUp)
[![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/) [![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/)
[![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) [![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server)
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv). 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)
![Tautulli Homepage](http://tautulli.com/images/screenshots/activity.png?v=2) ![Tautulli Homepage](http://tautulli.com/images/screenshots/activity-compressed.jpg?v=2)
## Installation and Support ## Installation 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

View File

@@ -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 () {

View File

@@ -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 &amp; 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>

View File

@@ -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;
} }

View File

@@ -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>
&middot; <a href="info?rating_key=${data['rating_key']}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a> &middot; <a href="${href}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
% elif data['media_type'] == 'track': % 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>

View File

@@ -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':

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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>

View File

@@ -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';

View File

@@ -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 () {

View File

@@ -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>&nbspFetching rows..."; var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching 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'] });

View File

@@ -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>&nbspLibraries 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>&nbspLibraries 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>&nbspUnable to refresh libraries list.', false, true, 2000, true); showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
}
} }
}); });
}); });

View File

@@ -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

View File

@@ -27,6 +27,16 @@
</button>&nbsp </button>&nbsp
</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>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</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() {

View File

@@ -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>&nbspUsers 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>&nbspUsers 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>&nbspUnable to refresh users list.', false, true, 2000, true); showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
}
} }
}); });
}); });

View File

@@ -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>

View File

@@ -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)

View File

@@ -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))

View File

@@ -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.")

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -188,7 +188,7 @@ class DataFactory(object):
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT, 'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT, 'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
'photo': 0, 'photo': 0,
'clip': plexpy.CONFIG.MOVIE_WATCHED_PERCENT 'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
} }
rows = [] rows = []
@@ -612,7 +612,6 @@ class DataFactory(object):
'total_plays': item['total_plays'], 'total_plays': item['total_plays'],
'total_duration': item['total_duration'], 'total_duration': item['total_duration'],
'last_play': item['last_watch'], 'last_play': item['last_watch'],
'thumb': user_thumb,
'user_thumb': user_thumb, 'user_thumb': user_thumb,
'grandparent_thumb': '', 'grandparent_thumb': '',
'art': '', 'art': '',
@@ -827,6 +826,9 @@ class DataFactory(object):
if session.get_session_shared_libraries(): if session.get_session_shared_libraries():
library_cards = session.get_session_shared_libraries() library_cards = session.get_session_shared_libraries()
if 'first_run_wizard' in library_cards:
return None
library_stats = [] library_stats = []
try: try:
@@ -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()

View File

@@ -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'])

View File

@@ -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):
""" """

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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:

View File

@@ -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...")

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.15-beta" PLEXPY_RELEASE_VERSION = "v2.0.19-beta"

View File

@@ -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:

View File

@@ -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.")

View File

@@ -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,

View File

@@ -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"
}, },
{...}, {...},
{...} {...}