Compare commits

...

75 Commits

Author SHA1 Message Date
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
e1e5a050c2 v2.0.15-beta 2018-01-27 11:08:45 -08:00
JonnyWong16
58996c1115 Unused now time 2018-01-27 10:59:48 -08:00
JonnyWong16
7301fe5f6e Remove 24 hour limit for recently added 2018-01-26 12:29:38 -08:00
JonnyWong16
a27c423569 Line up cards on the homepage 2018-01-24 21:37:02 -08:00
JonnyWong16
19680d3bc7 Refresh stream location on activity cards 2018-01-24 21:14:18 -08:00
JonnyWong16
ecaca4e5dc Change hover text from "View in" to "View on" 2018-01-24 21:07:12 -08:00
JonnyWong16
191de0b577 Add "View On" to Plex Web click-through 2018-01-24 21:04:34 -08:00
JonnyWong16
ebcc073b32 Add more server notification parameters. Rename plexpy parameters to tautulli. 2018-01-22 17:50:48 -08:00
JonnyWong16
043b3fd57b Update state for "Check server response" task 2018-01-22 13:44:51 -08:00
JonnyWong16
dd50502dcb Update Discord link to welcome channel 2018-01-22 11:27:04 -08:00
JonnyWong16
f159a1014d Don't add view_offset to live progress bar 2018-01-21 19:46:23 -08:00
JonnyWong16
abb801535c Add line break for Live progress 2018-01-21 16:09:48 -08:00
JonnyWong16
2732dbf1b1 Fix progress time for live tv 2018-01-21 16:07:32 -08:00
JonnyWong16
095d893005 Improve Live TV info on activity cards 2018-01-21 15:54:38 -08:00
JonnyWong16
5d8455d141 Get rating key for live sessions from websocket data 2018-01-21 13:09:02 -08:00
JonnyWong16
aa3450bfcc Add Labels and Collections to notification parameters 2018-01-20 20:01:01 -08:00
JonnyWong16
770f12b632 Hide advanced settings 2018-01-20 14:03:23 -08:00
JonnyWong16
45c2ccdffe v2.0.14-beta 2018-01-20 11:42:36 -08:00
JonnyWong16
fc14c3165f Remove email line break message 2018-01-20 11:30:11 -08:00
JonnyWong16
0fad245148 Try to cleanly shutdown loggers 2018-01-20 11:27:58 -08:00
JonnyWong16
79609c384e Show all changelogs when updated since previous version 2018-01-20 10:27:08 -08:00
JonnyWong16
09054ddb4b Correct clear logs message 2018-01-19 19:11:55 -08:00
JonnyWong16
6f912d4aa2 Add date header to Emails and do not add HTML line breaks automatically 2018-01-19 15:54:58 -08:00
JonnyWong16
96033a8214 Rename Tautulli update notification parameters 2018-01-19 14:59:00 -08:00
JonnyWong16
5ca65f4797 Catch json ValueError in metadata cache 2018-01-19 07:13:53 -08:00
JonnyWong16
d2fccbde68 Json dump custom conditions 2018-01-18 14:02:47 -08:00
JonnyWong16
e6b48d7baf Check for browser proxy compatibility 2018-01-17 21:02:31 -08:00
JonnyWong16
3e51310511 Re-enable browser notifications 2018-01-17 17:01:44 -08:00
JonnyWong16
32b43202c2 Attempt at fixing stuck sessions which require flishing the database 2018-01-15 18:55:37 -08:00
JonnyWong16
446170f8de Reduce websocket logging to playing and timeline only 2018-01-15 17:59:12 -08:00
JonnyWong16
c5a9ecd4ac Make sure websocket events are for library items 2018-01-15 14:49:49 -08:00
JonnyWong16
2af5f817a3 Plex Web url for tracks should go to the album page 2018-01-15 14:37:40 -08:00
JonnyWong16
4e55cf3cd4 Add all other bandwidth to WAN 2018-01-15 14:25:42 -08:00
JonnyWong16
eeb0478813 Use font-awesome arrow on activity cards 2018-01-14 20:45:27 -08:00
JonnyWong16
33739f1cb2 Fix check activity session write success 2018-01-13 21:16:07 -08:00
JonnyWong16
515e6a8071 Sort selectize when rendered 2018-01-13 17:47:24 -08:00
JonnyWong16
2b22f8eb4f Add select/remove all options for emails 2018-01-13 17:18:09 -08:00
44 changed files with 2506 additions and 1084 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@
*.db*
*.db-journal
*.ini
release.lock
version.lock
logs/*
cache/*

791
API.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,76 @@
# Changelog
## v2.0.18-beta (2018-02-12)
* Notifications:
* Fix: Default text for Tautulli update notifications using the wrong parameter.
* Fix: Playback pause and resume notifications only triggering once.
* Change: Negative operators for custom conditions now use "and" instead of "or".
* UI:
* New: Added button to delete the 3rd party lookup info from the info pages.
* Fix: Missing host info in the login logs when logging in using Firefox.
* Change: Cleaned up settings. Advanced settings are now hidden behind a toggle.
* API:
* New: Updated API documentation for v2.
* Other:
* Fix: DeprecationWarning when using HTTPS with self-signed certificates.
* Change: Deleting the Imgur poster URL also deletes the poster from Imgur (only available for new uploads).
* Change: GitHub repository moved to Tautulli/Tautulli. Old GitHub URLs will still work.
## v2.0.17-beta (2018-02-03)
* Notifications:
* Fix: Unable to use @ mentions tags for Discord and Slack.
* New: Added Zapier notification agent.
* API:
* Fix: get_synced_items returning no results.
* Fix: get_library_media_info returning incorrect media type for photo albums.
* Fix: get_library_media_info not being able to sort by title.
## v2.0.16-beta (2018-01-30)
* Monitoring:
* Fix: Timestamp sometimes showing as "0:60" on the activity cards.
* Fix: Incorrect session information being shown for playback of synced content.
* Fix: Sessions not being stopped when "Playback Stopped" notifications were enabled.
* UI:
* Fix: Stream resolution showing up as "unknown" on the graphs.
* New: Added user filter to the Synced Items table.
* Other:
* New: Option to use the Plex server update channel when checking for updates.
## v2.0.15-beta (2018-01-27)
* Monitoring:
* Fix: Live TV sessions not being stopped in History.
* Fix: Stream location showing as "unknown" on the activity cards.
* New: Improved Live TV details on the activity cards.
* Notifications:
* New: Added labels and collections to notification parameters.
* New: Added more server details to notification parameters.
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
## v2.0.14-beta (2018-01-20)
* Monitoring:
* Change: Added "Cellular" bandwidth to "WAN" in activity header.
* Notifications:
* Fix: Plex Web URL for tracks now go to the album page.
* Fix: Recently added notifications being sent for the entire library when DVR EPG data was refreshed.
* Fix: Notifier settings not loading with an apostrophe in the custom condition values.
* Fix: Custom email addresses not being saved when closing the notifier settings.
* Change: Re-enabled Browser notifications.
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
* Change: Emails no longer automatically insert HTML line breaks.
* Change: "Date" header added to email notifications.
* UI:
* Change: Show all changelogs since the previous version when updating.
## v2.0.13-beta (2018-01-13)
* Notifications:

View File

@@ -1,48 +1,7 @@
# Contributing to PlexPy
## 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.
# Contributing to Tautulli
## 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
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
#### 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.
#### 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
* 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.
#### 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
#### 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
* 4 space indentation

View File

@@ -8,7 +8,7 @@ Reporting Issues:
Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
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.
-->

View File

@@ -1,8 +1,8 @@
# Tautulli
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/36ggawe)
[![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/)
[![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).
@@ -27,56 +27,19 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
## 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 Guides](https://github.com/JonnyWong16/plexpy/wiki/Installation) shows you how to install Tautulli.
* [FAQs](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)) in the wiki can help you with common problems.
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
* 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
##### Many issues can simply be solved by:
- Making sure you update to the latest version.
- Turning your device off and on again.
- Analyzing your logs, you just might find the solution yourself!
- Using the **search** function to see if this issue has already been reported/solved.
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
- For basic questions try asking on [Discord](https://discord.gg/36ggawe), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
##### If nothing has worked:
1. Open a new issue on the GitHub [issue tracker](http://github.com/JonnyWong16/plexpy/issues).
2. Provide a clear title to easily help identify your problem.
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
4. Make sure you provide the following information:
- [ ] Version
- [ ] Branch
- [ ] Commit hash
- [ ] Operating system
- [ ] Python version
- [ ] What you did?
- [ ] What happened?
- [ ] What you expected?
- [ ] How can we reproduce your issue?
- [ ] What are your (relevant) settings?
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
## Feature Requests
Feature requests are handled on [FeatHub](http://feathub.com/JonnyWong16/plexpy).
1. Search the existing requests to see if your suggestion has already been submitted.
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
## License

View File

@@ -2,6 +2,7 @@
import plexpy
from plexpy import version
from plexpy.helpers import anon_url
from plexpy.notifiers import BROWSER_NOTIFIERS
%>
<!doctype html>
@@ -48,7 +49,7 @@
</div>
% 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;">
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 />
You're ${plexpy.COMMITS_BEHIND} commits behind.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
@@ -124,7 +125,7 @@
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></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="${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 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>
@@ -238,7 +239,7 @@ ${next.modalIncludes()}
<p>
Click the button below to continue to Flattr.
</p>
<a href="${anon_url('https://flattr.com/submit/auto?user_id=JonnyWong16&url=https://github.com/JonnyWong16/plexpy&title=Tautulli&language=en_GB&tags=github&category=software')}" target="_blank">
<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">
</a>
</div>
@@ -283,6 +284,9 @@ ${next.modalIncludes()}
<script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script>
% endif
<script>
% if _session['user_group'] == 'admin':
$('#updateDismiss').click(function() {

View File

@@ -22,11 +22,11 @@ DOCUMENTATION :: END
% if plexpy.CURRENT_VERSION:
<tr>
<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>
<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>
% endif
<tr>
@@ -75,18 +75,18 @@ DOCUMENTATION :: END
<td class="top-line">Resources:</td>
<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('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" 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" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Wiki &amp; FAQ</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" 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/%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/%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/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="feature request">FeatHub Feature Requests</a>
</td>
</tr>
<tr>
<td>Support:</td>
<td>
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/36ggawe')}" target="_blank">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/tQcWEUp')}" target="_blank">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://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>
</tr>
</tbody>

View File

@@ -108,6 +108,9 @@ select.form-control {
text-transform: uppercase;
font-size: 10px;
}
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values.negative-operator .value-wrapper:not(:first-child):before {
content: "and" !important;
}
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input {
padding-top: 3px !important;
padding-bottom: 3px !important;
@@ -131,19 +134,19 @@ select.form-control:focus,
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
fill: #999 !important;
}
.selectize-control .selectize-input > div .email {
.selectize-control .selectize-input > div .item-value {
opacity: 0.8;
font-size: 12px;
}
.selectize-control .selectize-input > div .user + .email {
.selectize-control .selectize-input > div .item-text + .item-value {
margin-left: 5px;
}
.selectize-control .selectize-input > div .email:before {
.selectize-control .selectize-input > div .item-value:before {
content: '<';
opacity: 0.8;
font-size: 12px;
}
.selectize-control .selectize-input > div .email:after {
.selectize-control .selectize-input > div .item-value:after {
content: '>';
opacity: 0.8;
font-size: 12px;
@@ -153,6 +156,25 @@ select.form-control:focus,
display: block;
color: #a0a0a0;
}
.selectize-control .selectize-dropdown .select-all,
.selectize-control .selectize-dropdown .remove-all {
font-weight: bold;
}
.selectize-control .selectize-dropdown .border-all {
pointer-events: none;
display: block;
height: 1px;
margin: 9px -12px 9px -12px;
padding: 0 !important;
overflow: hidden;
background-color: #e5e5e5;
}
.selectize-control .selectize-dropdown .border-all:last-child {
display: none;
}
.selectize-dropdown .optgroup-header {
font-weight: bold;
}
select.form-control option {
color: #555;
background-color: #fff;
@@ -201,7 +223,7 @@ object {
}
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
background-color: #2f2f2f;
border-color: none;
border-color: unset;
}
.dropdown-menu {
background-color: #282828;
@@ -687,8 +709,8 @@ a .users-poster-face:hover {
height: 290px;
min-width: 350px;
max-width: 500px;
margin-right: 20px;
margin-bottom: 20px;
margin-right: 25px;
margin-bottom: 25px;
}
.dashboard-activity-container {
height: 240px;
@@ -969,7 +991,6 @@ a .users-poster-face:hover {
background-image: -o-linear-gradient(top, #fbb450, #f89406);
background-image: linear-gradient(to bottom, #fbb450, #f89406);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
position: absolute;
height: 100%;
max-width: 100%;
@@ -1106,8 +1127,8 @@ a .dashboard-activity-metadata-user-thumb:hover {
height: 160px;
min-width: 350px;
max-width: 500px;
margin-right: 20px;
margin-bottom: 20px;
margin-right: 25px;
margin-bottom: 25px;
}
.dashboard-stats-container {
height: 160px;
@@ -1724,7 +1745,6 @@ a:hover .dashboard-recent-media-cover {
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3000000', endColorstr='#e6000000', GradientType=0);
webkit-box-shadow: inset 0 0 0 2px #e9a049;
-moz-box-shadow: inset 0 0 0 2px #e9a049;
box-shadow: inset 0 0 0 2px #e9a049;
@@ -1742,6 +1762,18 @@ a:hover .dashboard-recent-media-cover {
opacity: 0;
transition: opacity .3s;
}
.summary-poster-face-overlay span:before {
content: "View On";
color: #999;
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
text-align: center;
display: block;
position: absolute;
top: calc(50% - 34px);
width: 100%;
}
a:hover .summary-poster-face .summary-poster-face-overlay,
a:hover .summary-poster-face-episode .summary-poster-face-overlay,
a:hover .summary-poster-face-track .summary-poster-face-overlay,
@@ -2105,6 +2137,20 @@ a:hover .item-children-poster {
top: 5px;
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-poster-face {

View File

@@ -201,8 +201,8 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item">
<div class="sub-heading">Container</div>
<div class="sub-value" id="transcode_container-${sk}">
% if data.get('stream_container_decision') == 'transcode':
Transcode (${data['container'].upper()} &rarr; ${data['stream_container'].upper()})
% if data['stream_container_decision'] == 'transcode':
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
% else:
Direct Play (${data['container'].upper()})
% endif
@@ -213,13 +213,13 @@ DOCUMENTATION :: END
<div class="sub-heading">Video</div>
<div class="sub-value" id="video_decision-${sk}">
% if data['media_type'] in ('movie', 'episode', 'clip'):
% if data.get('stream_video_decision') == 'transcode':
% if data['stream_video_decision'] == 'transcode':
<%
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
%>
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} &rarr; ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% elif data.get('stream_video_decision') == 'copy':
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% elif data['stream_video_decision'] == 'copy':
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% else:
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
@@ -234,9 +234,9 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item">
<div class="sub-heading">Audio</div>
<div class="sub-value" id="audio_decision-${sk}">
% if data.get('stream_audio_decision') == 'transcode':
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} &rarr; ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% elif data.get('stream_audio_decision') == 'copy':
% if data['stream_audio_decision'] == 'transcode':
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% elif data['stream_audio_decision'] == 'copy':
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% else:
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
@@ -250,7 +250,7 @@ DOCUMENTATION :: END
<div class="sub-value" id="subtitle_decision-${sk}">
% if data['subtitles'] == 1:
% if data['stream_subtitle_decision'] == 'transcode':
Transcode (${data['subtitle_codec'].upper()} &rarr; ${data['stream_subtitle_codec'].upper()})
Transcode (${data['subtitle_codec'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()})
% elif data['stream_subtitle_decision'] == 'copy':
Direct Stream (${data['subtitle_codec'].upper()})
% elif data['stream_subtitle_decision'] == 'burn':
@@ -270,7 +270,7 @@ DOCUMENTATION :: END
<div class="sub-heading">Location</div>
<div class="sub-value time-right">
% if data['ip_address'] != 'N/A':
${data['location'].upper()}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
<span id="location-${sk}">${data['location'].upper()}</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']}">
<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>
@@ -312,7 +312,9 @@ DOCUMENTATION :: END
</div>
% if data['media_type'] != 'photo':
<div class="dashboard-activity-info-time">
% if data['view_offset']:
% if data['live'] == 1:
<br />Live
% elif data['view_offset']:
ETA:
<span id="stream-eta-${sk}">
<script>
@@ -340,8 +342,12 @@ DOCUMENTATION :: END
</div>
<div class="dashboard-activity-progress">
<div class="dashboard-activity-progress-bar">
% if data['live'] == 1:
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-toggle="tooltip" title="Stream Progress Live">Live</div>
% else:
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
% endif
</div>
</div>
</div>
@@ -389,7 +395,11 @@ DOCUMENTATION :: END
</div>
</div>
<div class="dashboard-activity-metadata-subtitle-container">
% if data['channel_stream'] == 0:
% if data['live'] == 1:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Plex Live TV">
<i class="fa fa-fw fa-television"></i>&nbsp;
</div>
% elif data['channel_stream'] == 0:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
% if data['media_type'] == 'movie':
<i class="fa fa-fw fa-film"></i>&nbsp;
@@ -404,12 +414,14 @@ DOCUMENTATION :: END
% endif
</div>
% else:
<div id="media-type-${sk}" title="Channel">
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Channel">
<i class="fa fa-fw fa-cloud"></i>&nbsp;
</div>
% endif
<div class="dashboard-activity-metadata-subtitle">
% if data['channel_stream'] == 0:
% if data['live'] == 1:
<span title="Plex Live TV" class="sub-heading">Plex Live TV</span>
% elif data['channel_stream'] == 0:
% if data['media_type'] == 'movie':
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
% elif data['media_type'] == 'episode':

View File

@@ -114,7 +114,7 @@
$.ajax({
url: 'get_user_names',
type: 'get',
dataType: "json",
dataType: 'json',
success: function (data) {
var select = $('#history-user');
data.sort(function (a, b) {
@@ -130,7 +130,6 @@
function loadHistoryTable(media_type, selected_user_id) {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function (d) {
return {
json_data: JSON.stringify(d),
@@ -138,9 +137,13 @@
user_id: selected_user_id
};
}
}
};
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');
clearSearchButton('history_table', history_table);
@@ -160,7 +163,7 @@
}
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);
% if _session['user_group'] == 'admin':

View File

@@ -131,12 +131,13 @@
<%def name="modalIncludes()">
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
<% from plexpy.common import VERSION_NUMBER %>
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Tautulli Updated</h4>
<h4 class="modal-title">Tautulli Updated to <strong>${VERSION_NUMBER}</strong></h4>
</div>
<div class="modal-body">
</div>
@@ -308,20 +309,23 @@
streams_header = streams_header.replace(/, $/, '') + ')';
$('#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) {
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) {
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').show();
sessions.forEach(function (session) {
var s = new Proxy(session, defaultHandler);
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
var key = s.session_key;
var session_id = s.session_id;
var instance = $('#activity-instance-' + key);
@@ -395,7 +399,7 @@
var transcode_container = '';
if (s.stream_container_decision === 'transcode') {
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' &rarr; ' + s.stream_container.toUpperCase() + ')';
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
} else {
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
}
@@ -428,7 +432,7 @@
if (s.stream_video_decision === 'transcode') {
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' &rarr; ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
} else if (s.stream_video_decision === 'copy') {
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
} else {
@@ -444,7 +448,7 @@
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
if (s.stream_audio_decision === 'transcode') {
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' &rarr; ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' <i class="fa fa-long-arrow-right"></i> ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
} else if (s.stream_audio_decision === 'copy') {
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
} else {
@@ -456,7 +460,7 @@
var subtitle_decision = 'None';
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
if (s.stream_subtitle_decision === 'transcode') {
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' &rarr; ' + s.stream_subtitle_codec.toUpperCase() + ')';
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')';
} else if (s.stream_subtitle_decision === 'copy') {
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
} else if (s.stream_subtitle_decision === 'burn') {
@@ -484,6 +488,8 @@
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
$('#location-' + key).html(s.location.toUpperCase());
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
var bw = parseInt(s.bandwidth);
if (bw !== "Unknown") {
@@ -509,7 +515,7 @@
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
var progress_bar = $('#progress-bar-' + key);
progress_bar.data('state', s.state);
if (progress_bar.data('last_view_offset') !== s.view_offset) {
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
}
@@ -817,7 +823,7 @@
$.ajax({
url: 'get_changelog',
data: {
latest_only: true,
since_prev_release: true,
update_shown: true
},
cache: false,

View File

@@ -117,9 +117,9 @@ DOCUMENTATION :: END
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track':
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
% else:
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
% endif
% if data['media_type'] == 'episode':
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
@@ -388,6 +388,15 @@ DOCUMENTATION :: END
</a>
</div>
% 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'):
<div class="btn-group">
% 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;">
% endif
<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']}">
<i class="fa fa-picture-o"></i> Reset Imgur Poster
data-id="${data['parent_rating_key'] if data['media_type'] in ('episode', 'track') else data['rating_key']}"
data-title="${data["poster_title"]}">
<i class="fa fa-picture-o"></i> Delete Imgur Poster
</button>
</span>
</div>
@@ -706,13 +716,28 @@ DOCUMENTATION :: END
});
$('#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 url = 'delete_poster_url';
var data = { rating_key: $(this).data('id') }
var msg = 'Are you sure you want to delete the Imgur poster for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
'All previous links to this image will no longer work.';
var url = 'delete_imgur_poster';
var data = { rating_key: $(this).data('id') };
var callback = function () {
$('.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);
});
</script>

View File

@@ -2,7 +2,7 @@
PNotify.prototype.options.addclass = "stack-bottomright";
PNotify.prototype.options.buttons.closer_hover = false;
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo.png' }
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo-circle.png' };
PNotify.prototype.options.history = false;
PNotify.prototype.options.shadow = false;
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
@@ -21,7 +21,7 @@ function check_notifications() {
$.getJSON('get_browser_notifications', function (data) {
if (data) {
$.each(data, function (i, notification) {
if (notification.delay == 0) {
if (notification.delay === 0) {
PNotify.prototype.options.hide = false;
} else {
PNotify.prototype.options.hide = true;
@@ -34,7 +34,7 @@ function check_notifications() {
setTimeout(function () {
"use strict";
check_notifications();
}, 3000);
}, 5000);
}
$(document).ready(function () {

View File

@@ -290,19 +290,9 @@ String.prototype.toProperCase = function () {
function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) {
seconds = ms / 1000;
minutes = seconds / 60;
if (roundToMinute) {
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;
var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0);
return (seconds == 60 ? (minutes+1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
} else {
if (roundToMinute) {
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>",
"emptyTable": "No data in table",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
},
},
"pagingType": "full_numbers",
"stateSave": true,
"processing": false,
@@ -172,7 +172,7 @@ history_table_options = {
},
"width": "33%",
"className": "datatable-wrap"
},
},
{
"targets": [7],
"data":"started",
@@ -322,7 +322,7 @@ history_table_options = {
$(row).addClass('current-activity-row');
}
}
}
};
// Parent table platform modal
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {

View File

@@ -98,7 +98,7 @@ sync_table_options = {
"data": "total_size",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData > 0 ) {
megabytes = Math.round((cellData/1024)/1024, 0)
megabytes = Math.round((cellData/1024)/1024, 0);
$(td).html(megabytes + 'MB');
} else {
$(td).html('0MB');
@@ -144,14 +144,16 @@ sync_table_options = {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0)
}
}
};
$('#sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
var tr = $(this).parents('tr');
var row = sync_table.row(tr);
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) {
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });

View File

@@ -385,8 +385,9 @@
$("#clear-logs").click(function () {
var logfile = $(".tab-pane.active").data('logfile')
var title = $("#log_tabs li.active a").text()
$("#confirm-message").text("Are you sure you want to clear the Tautulli logs?");
$("#confirm-message").text("Are you sure you want to clear the " + title + "?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({
@@ -421,7 +422,7 @@
});
$("#clear-notify-logs").click(function () {
$("#confirm-message").text("Are you sure you want to clear the Tautulli notification logs?");
$("#confirm-message").text("Are you sure you want to clear the Tautulli Notification Logs?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({
@@ -442,7 +443,7 @@
});
$("#clear-login-logs").click(function () {
$("#confirm-message").text("Are you sure you want to clear the Tautulli login logs?");
$("#confirm-message").text("Are you sure you want to clear the Tautulli Login Logs?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({

View File

@@ -98,6 +98,33 @@
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'selectize':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
</optgroup>
% endfor
% else:
<option value="border-all"></option>
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
% endif
</select>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% endif
% endfor
</div>
@@ -136,7 +163,7 @@
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
<label>Notification Conditions</label>
<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.
</p>
<div id="condition-widget"></div>
@@ -306,7 +333,7 @@
$('#notifier-config-modal').unbind('hidden.bs.modal');
// Need this for setting conditions since conditions contain the character "
$('#custom_conditions').val('${notifier['custom_conditions'] | n}')
$('#custom_conditions').val(${json.dumps(notifier["custom_conditions"]) | n});
$('#condition-widget').filterer({
parameters: ${parameters | n},
@@ -314,8 +341,23 @@
updateConditions: function(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() {
$.ajax({
url: 'get_notifier_config_modal',
@@ -332,7 +374,7 @@
if (jqXHR) {
var result = $.parseJSON(jqXHR.responseText);
var msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -392,7 +434,7 @@
% if notifier['agent_name'] == 'facebook':
function disableFacebookRequest() {
if ($('#facebook_app_id').val() != '' && $('#facebook_app_secret').val() != '') { $('#facebook_facebookStep1').prop('disabled', false); }
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebookStep1').prop('disabled', false); }
else { $('#facebook_facebookStep1').prop('disabled', true); }
}
disableFacebookRequest();
@@ -406,19 +448,20 @@
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
}
var facebook_token;
$.ajax({
url: 'facebookStep1',
data: {
app_id: $('#facebook_app_id').val(),
app_secret: $('#facebook_app_secret').val(),
redirect_uri: $('#facebook_redirect_uri').val(),
redirect_uri: $('#facebook_redirect_uri').val()
},
cache: false,
async: true,
complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = result.msg;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
window.open(result.url);
@@ -457,18 +500,18 @@
$('#notifier-config-modal').on('hidden.bs.modal', function () {
facebook_token = false;
})
});
% elif notifier['agent_name'] == 'browser':
$('#browser_allow_browser').click(function () {
PNotify.desktop.permission();
})
});
% elif notifier['agent_name'] == 'osx':
$('#osxnotifyregister').click(function () {
var osx_notify_app = $('#osx_notify_app').val();
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
})
});
% elif notifier['agent_name'] == 'email':
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
@@ -477,26 +520,37 @@
plugins: ['remove_button'],
persist: false,
maxItems: null,
valueField: 'email',
labelField: 'user',
searchField: ['user', 'email'],
options: ${json.dumps(user_emails) | n},
render: {
item: function(item, escape) {
return '<div>' +
(item.user ? '<span class="user">' + escape(item.user) + '</span>' : '') +
(item.email ? '<span class="email">' + escape(item.email) + '</span>' : '') +
(item.text ? '<span class="item-text">' + escape(item.text) + '</span>' : '') +
(item.value ? '<span class="item-value">' + escape(item.value) + '</span>' : '') +
'</div>';
},
option: function(item, escape) {
var label = item.user || item.email;
var caption = item.user ? item.email : null;
var label = item.text || item.value;
var caption = item.text ? item.value : null;
if (item.value.endsWith('-all')) {
return '<div class="' + item.value + '">' + escape(label) + '</div>'
}
return '<div>' +
escape(label) +
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
'</div>';
}
},
onItemAdd: function(value) {
if (value === 'select-all') {
var all_keys = $.map(this.options, function(option){
return option.value.endsWith('-all') ? null : option.value;
});
this.setValue(all_keys);
} else if (value === 'remove-all') {
this.clear();
this.refreshOptions();
this.positionDropdown();
}
},
createFilter: function(input) {
var match, regex;
@@ -514,16 +568,15 @@
},
create: function(input) {
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
return {email: input};
return {value: input};
}
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
if (match) {
return {
email : match[2],
user : $.trim(match[1])
value : match[2],
text : $.trim(match[1])
};
}
alert('Invalid email address.');
return false;
}
});
@@ -541,8 +594,19 @@
create: true
});
var join_device_names = $join_device_names[0].selectize;
console.log(${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
function validateLogic() {
@@ -673,7 +737,7 @@
});
function sendTestNotification() {
if ('${notifier["agent_name"]}' != 'browser') {
if ('${notifier["agent_name"]}' !== 'browser') {
$.ajax({
url: 'send_notification',
data: {
@@ -697,7 +761,7 @@
}
});
} else {
if ($('#browser_auto_hide_delay').val() == "0") {
if ($('#browser_auto_hide_delay').val() === "0") {
PNotify.prototype.options.hide = false;
} else {
PNotify.prototype.options.hide = true;

View File

@@ -42,7 +42,7 @@ DOCUMENTATION :: END
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
</tr>
% elif job in ('Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
% elif job in ('Check for server response', 'Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
<tr>
<td>${job}</td>
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>

View File

@@ -26,11 +26,16 @@
<span><i class="fa fa-cogs"></i> Settings</span>
</div>
<div class="button-bar">
% if config['show_advanced_settings'] == 1:
<button id="menu_link_show_advanced_settings" class="btn btn-dark active"><i class="fa fa-wrench"></i> Hide Advanced</button>
% else:
<button id="menu_link_show_advanced_settings" class="btn btn-dark"><i class="fa fa-wrench"></i> Show Advanced</button>
% endif
% if config['check_github']:
<button id="menu_link_update_check" class="btn btn-dark"><i class="fa fa-arrow-circle-up"></i> Check for Updates</button>
% endif
<button id="menu_link_restart" class="btn btn-dark"><i class="fa fa-refresh"></i> Restart</button>
<button id="menu_link_shutdown" class="btn btn-dark"><i class="fa fa-power-off"></i> Shut Down</button>
<button id="menu_link_shutdown" class="btn btn-dark"><i class="fa fa-power-off"></i> Shutdown</button>
</div>
</div>
</div>
@@ -43,19 +48,16 @@
<li role="presentation"><a href="#tabs-general" aria-controls="tabs-general" role="tab" data-toggle="tab">General</a></li>
<li role="presentation"><a href="#tabs-homepage" aria-controls="tabs-homepage" role="tab" data-toggle="tab">Homepage</a></li>
<li role="presentation"><a href="#tabs-web_interface" aria-controls="tabs-web_interface" role="tab" data-toggle="tab">Web Interface</a></li>
<li role="presentation"><a href="#tabs-access_control" aria-controls="tabs-access_control" role="tab" data-toggle="tab">Access Control</a></li>
<li role="presentation"><a href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
<li role="presentation"><a href="#tabs-plextv_account" aria-controls="tabs-plextv_account" role="tab" data-toggle="tab">Plex.tv Account</a></li>
<li role="presentation"><a href="#tabs-activity_monitoring" aria-controls="tabs-activity_monitoring" role="tab" data-toggle="tab">Activity Monitoring</a></li>
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications</a></li>
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
<li role="presentation"><a href="#tabs-extra_settings" aria-controls="tabs-extra_settings" role="tab" data-toggle="tab">Extra Settings</a></li>
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
</ul>
</div>
<div class="col-md-9">
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
<input type="hidden" id="show_advanced_settings" name="show_advanced_settings" value="${config['show_advanced_settings']}" required>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-help_info">
% if common.VERSION_NUMBER:
@@ -78,6 +80,7 @@
<br>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-general">
<div class="padded-header">
@@ -102,24 +105,54 @@
</div>
<p class="help-block">Set your preferred time format. <a href="javascript:void(0)" data-target="#dateTimeOptionsModal" data-toggle="modal">Click here</a> to see the parameter list.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="week_start_monday" name="week_start_monday" value="1" ${config['week_start_monday']}> Week Starting on Monday
</label>
<p class="help-block">Change the "<em>Play by day of week</em>" graph to start on Monday. Default is start on Sunday.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Table and Watch Statistics History
</label>
<p class="help-block">Group successive play history by the same user as a single entry in the tables and watch statistics.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="history_table_activity" name="history_table_activity" value="1" ${config['history_table_activity']}> Current Activity in History Tables
</label>
<p class="help-block">Include current activity in the history tables. Statistics will not be counted until the stream has ended.</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes
</label>
<p class="help-block">Enable if you want Tautulli to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="log_blacklist" name="log_blacklist" value="1" ${config['log_blacklist']}> Mask Sensitive Information in Logs
</label>
<p class="help-block">
Enable to mask passwords, access tokens, and public IP addresses with asterisks (*) in the logs.<br />
Note: Only logs from the time this setting is enabled will be masked. Do not post your logs publically without masking sensitive information!
</p>
</div>
<div class="padded-header">
<h3>History Logging</h3>
</div>
<div class="form-group advanced-setting">
<label for="logging_ignore_interval">Ignore Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="logging_ignore_interval" name="logging_ignore_interval" value="${config['logging_ignore_interval']}" size="5" data-parsley-min="0" data-parsley-trigger="change" data-parsley-errors-container="#logging_ignore_interval_error" required>
</div>
<div id="logging_ignore_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.</p>
</div>
<div class="form-group">
<label for="movie_watched_percent">Movie Watched Percent</label>
<div class="row">
@@ -150,6 +183,20 @@
</div>
<p class="help-block">Set the percentage for a music track to be considered as listened. Minimum 50, Maximum 95.</p>
</div>
<div class="form-group advanced-setting">
<label>Flush Temporary Sessions</label>
<p class="help-block">
Attempt to fix history logging by flushing out all of the temporary sessions in the database.<br />
Warning: This will reset all currently active sessions. For emergency use only when history logging is stuck!
</p>
<div class="row">
<div class="col-md-4">
<div class="btn-group">
<button class="btn btn-form" type="button" id="delete_temp_sessions">Flush</button>
</div>
</div>
</div>
</div>
<div class="padded-header">
<h3>Updates</h3>
@@ -168,7 +215,7 @@
</label>
<p class="help-block">Update Tautulli automatically if an update is available.</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="git_token">GitHub API Token</label>
<div class="row">
<div class="col-md-6">
@@ -179,6 +226,41 @@
</div>
</div>
% if plexpy.INSTALL_TYPE == 'git':
<div class="form-group advanced-setting">
<label for="git_branch">Git Remote / Branch</label>
<div class="row">
<div class="col-md-6">
<div class="input-group git-group">
<input type="text" class="form-control" id="git_remote" name="git_remote" value="${config['git_remote']}" data-parsley-trigger="change">
<select class="form-control" id="git_branch" name="git_branch">
<% branches = ('master', 'beta', 'nightly') %>
% for branch in branches:
<option value="${branch}" ${'selected' if config['git_branch'] == branch else ''}>${branch}</option>
% endfor
% if config['git_branch'] not in branches:
<option value="${config['git_branch']}" selected>${config['git_branch']}</option>
% endif
</select>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="switch_git_branch">Checkout Branch</button>
</span>
</div>
</div>
</div>
<p class="help-block">The git tracking remote and branch (default "origin/master"). Select to switch the git branch (requires restart).</p>
</div>
<div class="form-group advanced-setting">
<label for="git_path">Git Path</label>
<div class="row">
<div class="col-md-4">
<input type="text" class="form-control" id="git_path" name="git_path" value="${config['git_path']}" size="30">
</div>
</div>
<p class="help-block">Optional: The path to your git environment variable. Leave blank for default.</p>
</div>
% endif
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
@@ -331,7 +413,13 @@
</div>
<p class="help-block">Note: Web interface changes require a restart.</p>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" ${config['launch_browser']}> Launch Browser on Startup
</label>
<p class="help-block">Launch browser pointed to Tautulli on startup.</p>
</div>
<div class="form-group advanced-setting">
<label for="http_host">HTTP Host</label>
<div class="row">
<div class="col-md-6">
@@ -350,7 +438,7 @@
</div>
<p class="help-block">Port to bind web server to. Note that ports below 1024 may require root.</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="http_root">HTTP Root</label>
<div class="row">
<div class="col-md-6">
@@ -359,34 +447,27 @@
</div>
<p class="help-block">The base URL of the web server. Used for reverse proxies.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" class="http-settings" name="http_proxy" id="http_proxy" value="1" ${config['http_proxy']}> Enable HTTP Proxy
</label>
<p class="help-block">Respect the X-Forwarded-Proto header. Used for reverse proxies with SSL.</p>
</div>
<br />
<div class="checkbox">
<label>
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" ${config['launch_browser']}> Launch Browser on Startup
</label>
<p class="help-block">Launch browser pointed to Tautulli on startup.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" class="http-settings" name="enable_https" id="enable_https" value="1" ${config['enable_https']} /> Enable HTTPS
</label>
<p class="help-block">Enable HTTPS for web server for encrypted communication.</p>
</div>
<div id="https_options">
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" class="http-settings" name="https_create_cert" id="https_create_cert" value="1" ${config['https_create_cert']} /> Create Self-signed Certificate
</label>
<p class="help-block">Check to have Tautulli create a self-signed SSL certificate. Uncheck if you want to use your own certificate.</p>
</div>
<div id="https_options_self-signed">
<div class="form-group">
<div class="form-group advanced-setting">
<label for="https_domain">HTTPS Domains</label>
<div class="row">
<div class="col-md-6">
@@ -395,7 +476,7 @@
</div>
<p class="help-block">The domain names used to access Tautulli, separated by commas (,).</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="https_ip">HTTPS IPs</label>
<div class="row">
<div class="col-md-6">
@@ -405,7 +486,7 @@
<p class="help-block">The IP addresses used to access Tautulli, separated by commas (,).</p>
</div>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="https_cert">HTTPS Certificate</label>
<div class="row">
<div class="col-md-6">
@@ -414,7 +495,7 @@
</div>
<p class="help-block">The location of the SSL certificate.</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="https_cert_chain">HTTPS Certificate Chain</label>
<div class="row">
<div class="col-md-6">
@@ -423,7 +504,7 @@
</div>
<p class="help-block">The location of the SSL certificate chain.</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="https_key">HTTPS Key</label>
<div class="row">
<div class="col-md-6">
@@ -433,17 +514,22 @@
<p class="help-block">The location of the SSL key.</p>
</div>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-access_control">
<div class="form-group advanced-setting">
<label for="anon_redirect">Anonymous Redirect</label>
<div class="row">
<div class="col-md-4">
<input type="text" class="form-control" id="anon_redirect" name="anon_redirect" value="${config['anon_redirect']}" size="30">
</div>
</div>
<p class="help-block">Backlink protection via anonymizer service, must end in "?".</p>
</div>
<div class="padded-header">
<h3>Authentication</h3>
</div>
<p class="help-block">Authentication changes require a restart.</p>
<p class="help-block">Note: Authentication changes require a restart.</p>
<div class="form-group">
<label for="http_username">HTTP Username</label>
<div class="row">
@@ -463,23 +549,17 @@
</div>
<p class="help-block">Password for web server authentication. Leave empty to disable.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" name="http_hash_password" id="http_hash_password" value="1" ${config['http_hash_password']} data-parsley-trigger="change"> Hash Password in the Config File
</label>
<span id="hashPasswordCheck" style="color: #eb8600; padding-left: 10px;"></span>
<p class="help-block">Store a hashed password in the config file.<br />Warning: Your password cannot be recovered if forgotten!</p>
</div>
<input type="text" id="http_hashed_password" name="http_hashed_password" value="${config['http_hashed_password']}" style="display: none;" data-parsley-trigger="change" data-parsley-type="integer" data-parsley-range="[0, 1]"
<input type="text" id="http_hashed_password" name="http_hashed_password" value="${config['http_hashed_password']}" style="display: none;" data-parsley-trigger="change" data-parsley-type="integer" data-parsley-range="[0, 1]"
data-parsley-errors-container="#http_hash_password_error" data-parsley-error-message="Cannot un-hash password, please set a new password." data-parsley-no-focus required>
<div class="checkbox">
<label>
<input type="checkbox" class="auth-settings" name="http_plex_admin" id="http_plex_admin" value="1" ${config['http_plex_admin']} data-parsley-trigger="change"> Allow Plex Admin
</label>
<span id="allowPlexCheck" style="color: #eb8600; padding-left: 10px;"></span>
<p class="help-block">Allow the Plex server admin to login as a Tautulli admin using their Plex.tv account.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" class="auth-settings" name="http_basic_auth" id="http_basic_auth" value="1" ${config['http_basic_auth']} data-parsley-trigger="change"> Use Basic Authentication
</label>
@@ -488,10 +568,13 @@
<input type="checkbox" name="auth_changed" id="auth_changed" value="1" style="display: none;">
<div class="padded-header">
<h3>Guest Access</h3>
<div class="checkbox">
<label>
<input type="checkbox" class="auth-settings" name="http_plex_admin" id="http_plex_admin" value="1" ${config['http_plex_admin']} data-parsley-trigger="change"> Allow Plex Admin
</label>
<span id="allowPlexCheck" style="color: #eb8600; padding-left: 10px;"></span>
<p class="help-block">Allow the Plex server admin to login as a Tautulli admin using their Plex.tv account.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="allow_guest_access" name="allow_guest_access" value="1" ${config['allow_guest_access']}> Allow Guest Access to Tautulli
@@ -537,52 +620,6 @@
<h3>Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
</div>
<div class="checkbox">
% if config['pms_is_cloud']:
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" disabled> Monitor Plex Updates
</label>
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
% else:
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
</label>
% endif
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
</div>
<div id="pms_update_options">
<div class="form-group">
<div class="row">
<div class="col-md-2">
<label for="pms_update_channel">Update Channel</label>
<select class="form-control" id="pms_update_channel" name="pms_update_channel">
<option value="public">Public</option>
</select>
</div>
<div class="col-md-5">
<label for="pms_update_distro_build">Release</label>
<select class="form-control" id="pms_update_distro_build" name="pms_update_distro_build">
</select>
<input type="hidden" class="form-control" id="pms_update_distro" name="pms_update_distro">
</div>
</div>
</div>
</div>
<div class="checkbox">
% if config['pms_is_cloud']:
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" disabled> Monitor Plex Remote Access
</label>
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
% else:
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
</label>
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
% endif
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div>
<div class="form-group has-feedback" id="pms_ip_group">
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
@@ -621,7 +658,7 @@
</label>
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
% if config['pms_is_cloud']:
<label>
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" disabled> Manual Connection
@@ -634,7 +671,7 @@
% endif
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="pms_logs_folder">Plex Web URL</label>
<div class="row">
<div class="col-md-6">
@@ -655,11 +692,7 @@
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
<div class="padded-header">
<h3>Plex Logs</h3>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="pms_logs_folder">Logs Folder</label>
<div class="row">
<div class="col-md-6">
@@ -673,11 +706,100 @@
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank">Click here</a> for help.
</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="cache_images" name="cache_images" value="1" ${config['cache_images']}> Cache Plex Images
</label>
<p class="help-block">
Enable to cache images from Plex to reduce API calls and improve loading times.<br />
Note: Video preview thumbnails (BIF) are not cached.
</p>
</div>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<div class="padded-header">
<h3>Server Monitoring</h3>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-plextv_account">
<div class="checkbox">
% if config['pms_is_cloud']:
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" disabled> Monitor Plex Updates
</label>
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
% else:
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
</label>
% endif
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
</div>
<div id="pms_update_options">
<div class="form-group">
<div class="row">
<div class="col-md-3">
<label for="pms_update_channel">Update Channel</label>
<select class="form-control" id="pms_update_channel" name="pms_update_channel">
<option value="plex">Use Server Setting</option>
<option value="public">Public</option>
</select>
</div>
<div class="col-md-5">
<label for="pms_update_distro_build">Release</label>
<select class="form-control" id="pms_update_distro_build" name="pms_update_distro_build">
</select>
<input type="hidden" class="form-control" id="pms_update_distro" name="pms_update_distro">
</div>
</div>
</div>
</div>
<div class="checkbox">
% if config['pms_is_cloud']:
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" disabled> Monitor Plex Remote Access
</label>
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
% else:
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
</label>
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
% endif
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div>
<div class="form-group advanced-setting">
<label for="refresh_users_interval">Users List Refresh Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_users_interval" name="refresh_users_interval" value="${config['refresh_users_interval']}" size="5" data-parsley-range="[1,24]" data-parsley-trigger="change" data-parsley-errors-container="#refresh_users_interval_error" required>
</div>
<div id="refresh_users_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in hours) Tautulli will request an updated friends list from Plex.tv. Minimum 1, maximum 24, default 12.</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="refresh_users_on_startup" name="refresh_users_on_startup" value="1" ${config['refresh_users_on_startup']}> Refresh Users List on Startup
</label>
<p class="help-block">Refresh the users list when Tautulli starts.</p>
</div>
<div class="form-group advanced-setting">
<label for="refresh_libraries_interval">Libraries List Refresh Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_libraries_interval" name="refresh_libraries_interval" value="${config['refresh_libraries_interval']}" size="5" data-parsley-range="[1,24]" data-parsley-trigger="change" data-parsley-errors-container="#refresh_libraries_interval_error" required>
</div>
<div id="refresh_libraries_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in hours) Tautulli will request an updated libraries list from your Plex Media Server. Minimum 1, maximum 24, default 12.</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="refresh_libraries_on_startup" name="refresh_libraries_on_startup" value="1" ${config['refresh_libraries_on_startup']}> Refresh Libraries List on Startup
</label>
<p class="help-block">Refresh the libraries list when Tautulli starts.</p>
</div>
<div class="padded-header">
<h3>Plex.tv Authentication</h3>
@@ -699,71 +821,14 @@
<p class="help-block">Token for Plex.tv authentication.</p>
</div>
<div class="padded-header">
<h3>Friends List</h3>
</div>
<div class="form-group">
<label for="refresh_users_interval">Users List Refresh Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_users_interval" name="refresh_users_interval" value="${config['refresh_users_interval']}" size="5" data-parsley-range="[1,24]" data-parsley-trigger="change" data-parsley-errors-container="#refresh_users_interval_error" required>
</div>
<div id="refresh_users_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in hours) Tautulli will request an updated friends list from Plex.tv. Minimum 1, maximum 24, default 12.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="refresh_users_on_startup" name="refresh_users_on_startup" value="1" ${config['refresh_users_on_startup']}> Refresh Users List on Startup
</label>
<p class="help-block">Refresh the users list when Tautulli starts.</p>
</div>
<div class="padded-header">
<h3>Libraries List</h3>
</div>
<div class="form-group">
<label for="refresh_libraries_interval">Libraries List Refresh Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_libraries_interval" name="refresh_libraries_interval" value="${config['refresh_libraries_interval']}" size="5" data-parsley-range="[1,24]" data-parsley-trigger="change" data-parsley-errors-container="#refresh_libraries_interval_error" required>
</div>
<div id="refresh_libraries_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in hours) Tautulli will request an updated libraries list from your Plex Media Server. Minimum 1, maximum 24, default 12.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="refresh_libraries_on_startup" name="refresh_libraries_on_startup" value="1" ${config['refresh_libraries_on_startup']}> Refresh Libraries List on Startup
</label>
<p class="help-block">Refresh the libraries list when Tautulli starts.</p>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-activity_monitoring">
<div role="tabpanel" class="tab-pane" id="tabs-notifications">
<div class="padded-header">
<h3>History Logging</h3>
</div>
<div class="form-group">
<label for="logging_ignore_interval">Ignore Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="logging_ignore_interval" name="logging_ignore_interval" value="${config['logging_ignore_interval']}" size="5" data-parsley-min="0" data-parsley-trigger="change" data-parsley-errors-container="#logging_ignore_interval_error" required>
</div>
<div id="logging_ignore_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.</p>
</div>
<div class="padded-header">
<h3>Buffer Warnings</h3>
<h3>Current Activity Notifications</h3>
</div>
<p class="help-block">Note: Buffer warnings only work on certain Plex clients. Android and Plex Web do not report buffer events accurately or at all.</p>
@@ -777,7 +842,7 @@
</div>
<p class="help-block">How many buffer events should we wait before triggering the first warning. Buffer events increment on each monitor ping if play state is buffering. 0 to disable buffer warnings.</p>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label for="buffer_wait">Buffer Wait</label>
<div class="row">
<div class="col-md-2">
@@ -787,24 +852,13 @@
</div>
<p class="help-block">The value (in seconds) Tautulli should wait before triggering the next buffer warning. 0 to always trigger.</p>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-notifications">
<div class="padded-header">
<h3>Current Activity Notifications</h3>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Consecutive Notifications
</label>
<p class="help-block">Enable to allow sending of consecutive notifications (i.e. both watched &amp; stopped notifications).</p>
</div>
<div class="checkbox">
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" name="notify_concurrent_by_ip" id="notify_concurrent_by_ip" value="1" ${config['notify_concurrent_by_ip']}> User Concurrent Streams Notifications by IP Address
</label>
@@ -920,103 +974,6 @@
</div>
<div role="tabpanel" class="tab-pane" id="tabs-extra_settings">
<div class="padded-header">
<h3>Extra Settings</h3>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes
</label>
<p class="help-block">Enable if you want Tautulli to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="log_blacklist" name="log_blacklist" value="1" ${config['log_blacklist']}> Mask Sensitive Information in Logs
</label>
<p class="help-block">
Enable to mask passwords, access tokens, and public IP addresses with asterisks (*) in the logs.<br />
Note: Only logs from the time this setting is enabled will be masked. Do not post your logs publically without masking sensitive information!
</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="cache_images" name="cache_images" value="1" ${config['cache_images']}> Cache Plex Images
</label>
<p class="help-block">
Enable to cache images from Plex to reduce API calls and improve loading times.<br />
Note: Video preview thumbnails (BIF) are not cached.
</p>
</div>
<div class="form-group">
<label for="anon_redirect">Anonymous Redirect</label>
<div class="row">
<div class="col-md-4">
<input type="text" class="form-control" id="anon_redirect" name="anon_redirect" value="${config['anon_redirect']}" size="30">
</div>
</div>
<p class="help-block">Backlink protection via anonymizer service, must end in "?".</p>
</div>
<div class="form-group">
<label>Flush Temporary Sessions</label>
<p class="help-block">
Attempt to fix history logging by flushing out all of the temporary sessions in the database.<br />
Warning: This will reset all currently active sessions. For emergency use only when history logging is stuck!
</p>
<div class="row">
<div class="col-md-4">
<div class="btn-group">
<button class="btn btn-form" type="button" id="delete_temp_sessions">Flush</button>
</div>
</div>
</div>
</div>
% if plexpy.INSTALL_TYPE == 'git':
<div class="padded-header">
<h3>Git Settings</h3>
</div>
<div class="form-group">
<label for="git_branch">Git Remote / Branch</label>
<div class="row">
<div class="col-md-6">
<div class="input-group git-group">
<input type="text" class="form-control" id="git_remote" name="git_remote" value="${config['git_remote']}" data-parsley-trigger="change">
<select class="form-control" id="git_branch" name="git_branch">
<% branches = ('master', 'beta', 'nightly') %>
% for branch in branches:
<option value="${branch}" ${'selected' if config['git_branch'] == branch else ''}>${branch}</option>
% endfor
% if config['git_branch'] not in branches:
<option value="${config['git_branch']}" selected>${config['git_branch']}</option>
% endif
</select>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="switch_git_branch">Checkout Branch</button>
</span>
</div>
</div>
</div>
<p class="help-block">The git tracking remote and branch (default "origin/master"). Select to switch the git branch (requires restart).</p>
</div>
<div class="form-group">
<label for="git_path">Git Path</label>
<div class="row">
<div class="col-md-4">
<input type="text" class="form-control" id="git_path" name="git_path" value="${config['git_path']}" size="30">
</div>
</div>
<p class="help-block">Optional: The path to your git environment variable. Leave blank for default.</p>
</div>
% endif
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
<div class="padded-header">
@@ -1146,8 +1103,8 @@
</div>
<div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
<strong>Please read the <a href="${anon_url('https://github.com/%s/plexpy/blob/master/CONTRIBUTING.md' % plexpy.CONFIG.GIT_USER)}" target="_blank">guidelines</a>
in the CONTRIBUTING document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Issues/blob/master/README.md' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">guidelines</a>
in the README document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
<br /><br />
Your post may be removed for failure to follow the guidelines.
</div>
@@ -1168,7 +1125,7 @@
</div>
<div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
<strong>Please read the <a href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank">FAQ</a>
<strong>Please read the <a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">FAQ</a>
before asking for help!</strong>
</div>
</div>
@@ -1343,13 +1300,22 @@
</div>
<div class="modal-body">
<div>
<div style="padding-bottom: 10px;">
<p class="help-block">
Note: Tags separate the <em>media type</em> that triggered the notifications (i.e. a complete show added to Plex vs. a single episode added to Plex).
They <em>do not</em> separate the notification parameters (i.e. <span class="inline-pre">{show_name}</span> vs. <span class="inline-pre">{episode_name}</span>.
</p>
<p class="help-block">
Note: Nesting tags inside each other is not supported.
</p>
</div>
<div>
<h4>Movie Tag</h4>
</div>
<div style="padding-bottom: 10px;">
<p class="help-block">All text inside <span class="inline-pre">&lt;movie&gt;&lt;/movie&gt;</span> tags will only be sent when the media item is a movie.</p>
<p class="help-block">All text inside <span class="inline-pre">&lt;movie&gt;&lt;/movie&gt;</span> tags will only be sent when the media type is movie.</p>
<p><strong style="color: #fff;">Example:</strong></p>
<pre>{title} &lt;movie&gt;({year})&lt;/movie&gt; was recently added to Plex</pre>
<pre>{title}&lt;movie&gt;({year})&lt;/movie&gt; was recently added to Plex</pre>
</div>
<div>
<h4>Show / Season / Episode Tags</h4>
@@ -1357,7 +1323,7 @@
<div style="padding-bottom: 10px;">
<p class="help-block">
All text inside <span class="inline-pre">&lt;show&gt;&lt;/show&gt;</span>/<span class="inline-pre">&lt;season&gt;&lt;/season&gt;</span>/<span class="inline-pre">&lt;episode&gt;&lt;/episode&gt;</span>
tags will only be sent when the media item is a show/season/episode.
tags will only be sent when the media type is show/season/episode.
</p>
<p><strong style="color: #fff;">Example:</strong></p>
<pre>{show_name}&lt;season&gt; - Season {season_num}&lt;/season&gt;&lt;episode&gt; - S{season_num}E{episode_num} - {episode_name}&lt;/episode&gt; was recently added to Plex.</pre>
@@ -1368,7 +1334,7 @@
<div>
<p class="help-block">
All text inside <span class="inline-pre">&lt;artist&gt;&lt;/artist&gt;</span>/<span class="inline-pre">&lt;album&gt;&lt;/album&gt;</span>/<span class="inline-pre">&lt;track&gt;&lt;/track&gt;</span>
tags will only be sent when the media item is an artist/album/track.
tags will only be sent when the media type is artist/album/track.
</p>
<p><strong style="color: #fff;">Example:</strong></p>
<pre>{artist_name}&lt;album&gt; - {album_name}&lt;/album&gt;&lt;track&gt; - {album_name} - {track_name}&lt;/track&gt; was recently added to Plex.</pre>
@@ -1658,6 +1624,15 @@ $(document).ready(function() {
}
}
function advancedSettings() {
var advanced_button = $('#menu_link_show_advanced_settings');
if (advanced_button.hasClass('active')) {
$('.advanced-setting').show();
} else {
$('.advanced-setting').hide();
}
}
$('.save-button').click(function() {
preSaveChecks(function () { saveSettings() });
});
@@ -1697,6 +1672,19 @@ $(document).ready(function() {
window.location.href = 'restart';
});
$('#menu_link_show_advanced_settings').click(function() {
$(this).toggleClass('active');
if ($(this).hasClass('active')) {
$(this).html('<i class="fa fa-wrench"></i> Hide Advanced');
$('#show_advanced_settings').val(1);
} else {
$(this).html('<i class="fa fa-wrench"></i> Show Advanced');
$('#show_advanced_settings').val(0);
}
advancedSettings()
});
advancedSettings();
getConfigurationTable();
getSchedulerTable();
getNotifiersTable();
@@ -2104,32 +2092,41 @@ $(document).ready(function() {
var update_channel = update_params.pms_update_channel;
var update_distro = update_params.pms_update_distro;
var update_distro_build = update_params.pms_update_distro_build;
var plex_update_channel = update_params.plex_update_channel;
$("#pms_update_channel option[value='plexpass']").remove();
$('#pms_update_channel option[value=beta]').remove();
if (plexpass) {
var selected = (update_channel == 'plexpass') ? true : false;
var selected = (update_channel == 'beta') ? true : false;
$('#pms_update_channel')
.append($('<option></option>')
.text('Plex Pass')
.val('plexpass')
.text('Beta')
.val('beta')
.prop('selected', selected));
}
$.getJSON('https://plex.tv/api/downloads/1.json?channel=' + update_channel, function (downloads) {
platform_downloads = downloads.computer[platform] || downloads.nas[platform];
if (platform_downloads) {
$("#pms_update_distro_build option").remove();
$.each(platform_downloads.releases, function (index, item) {
var label = (platform_downloads.releases.length == 1) ? platform_downloads.name : platform_downloads.name + ' - ' + item.label;
var selected = (item.distro == update_distro && item.build == update_distro_build) ? true : false;
$('#pms_update_distro_build')
.append($('<option></option>')
.text(label)
.val(item.build)
.attr('data-distro', item.distro)
.prop('selected', selected));
})
$('#pms_update_distro').val($("#pms_update_distro_build option:selected").data('distro'))
$.ajax({
url: 'https://plex.tv/api/downloads/1.json?channel=' + plex_update_channel,
type: 'GET',
dataType: 'json',
beforeSend: function (xhr) {
xhr.setRequestHeader('X-Plex-Token', $('#pms_token').val());
},
success: function (downloads) {
var platform_downloads = downloads.computer[platform] || downloads.nas[platform];
if (platform_downloads) {
$("#pms_update_distro_build option").remove();
$.each(platform_downloads.releases, function (index, item) {
var label = (platform_downloads.releases.length === 1) ? platform_downloads.name : platform_downloads.name + ' - ' + item.label;
var selected = (item.distro === update_distro && item.build === update_distro_build) ? true : false;
$('#pms_update_distro_build')
.append($('<option></option>')
.text(label)
.val(item.build)
.attr('data-distro', item.distro)
.prop('selected', selected));
});
$('#pms_update_distro').val($('#pms_update_distro_build option:selected').data('distro'))
}
}
});
});

View File

@@ -27,6 +27,16 @@
</button>&nbsp
</div>
% 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">
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
</div>
@@ -87,17 +97,45 @@
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
<script>
$(document).ready(function() {
sync_table_options.ajax = {
url: 'get_sync',
data: function (d) {
d.user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
// Load user ids and names (for the selector)
$.ajax({
url: 'get_user_names',
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':
$('#row-edit-mode').on('click', function() {

View File

@@ -15,6 +15,7 @@
import os
from Queue import Queue
import shutil
import sqlite3
import sys
import subprocess
@@ -34,7 +35,7 @@ from apscheduler.triggers.interval import IntervalTrigger
import activity_handler
import activity_pinger
import config
import common
import database
import libraries
import logger
@@ -42,7 +43,6 @@ import mobile_app
import notification_handler
import notifiers
import plextv
import pmsconnect
import users
import versioncheck
import plexpy.config
@@ -83,6 +83,7 @@ INSTALL_TYPE = None
CURRENT_VERSION = None
LATEST_VERSION = None
COMMITS_BEHIND = None
PREV_RELEASE = None
UMASK = None
@@ -102,7 +103,9 @@ def initialize(config_file):
global _INITIALIZED
global CURRENT_VERSION
global LATEST_VERSION
global PREV_RELEASE
global UMASK
CONFIG = plexpy.config.Config(config_file)
CONFIG_FILE = config_file
@@ -154,6 +157,16 @@ def initialize(config_file):
except OSError as e:
logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e))
if CONFIG.CACHE_DIR:
session_metadata_folder = os.path.join(CONFIG.CACHE_DIR, 'session_metadata')
try:
shutil.rmtree(session_metadata_folder, ignore_errors=True)
except OSError as e:
pass
if not os.path.exists(session_metadata_folder):
os.mkdir(session_metadata_folder)
# Initialize the database
logger.info(u"Checking if the database upgrades are required...")
try:
@@ -190,6 +203,17 @@ def initialize(config_file):
CONFIG.JWT_SECRET = generate_uuid()
CONFIG.write()
# Get the previous version from the file
version_lock_file = os.path.join(DATA_DIR, "version.lock")
prev_version = None
if os.path.isfile(version_lock_file):
try:
with open(version_lock_file, "r") as fp:
prev_version = fp.read()
except IOError as e:
logger.error(u"Unable to read previous version from file '%s': %s" %
(version_lock_file, e))
# Get the currently installed version. Returns None, 'win32' or the git
# hash.
CURRENT_VERSION, CONFIG.GIT_REMOTE, CONFIG.GIT_BRANCH = versioncheck.getVersion()
@@ -198,8 +222,6 @@ def initialize(config_file):
# This allowes one to restore to that version. The idea is that if we
# arrive here, most parts of Tautulli seem to work.
if CURRENT_VERSION:
version_lock_file = os.path.join(DATA_DIR, "version.lock")
try:
with open(version_lock_file, "w") as fp:
fp.write(CURRENT_VERSION)
@@ -217,6 +239,32 @@ def initialize(config_file):
else:
LATEST_VERSION = CURRENT_VERSION
# Get the previous release from the file
release_file = os.path.join(DATA_DIR, "release.lock")
PREV_RELEASE = common.VERSION_NUMBER
if os.path.isfile(release_file):
try:
with open(release_file, "r") as fp:
PREV_RELEASE = fp.read()
except IOError as e:
logger.error(u"Unable to read previous release from file '%s': %s" %
(release_file, e))
elif prev_version == 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca': # Commit hash for v1.4.25
PREV_RELEASE = 'v1.4.25'
# Check if the release was updated
if common.VERSION_NUMBER != PREV_RELEASE:
CONFIG.UPDATE_SHOW_CHANGELOG = 1
CONFIG.write()
# Write current release version to file for update checking
try:
with open(release_file, "w") as fp:
fp.write(common.VERSION_NUMBER)
except IOError as e:
logger.error(u"Unable to write current release to file '%s': %s" %
(release_file, e))
# Get the real PMS urls for SSL and remote access
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
plextv.get_server_resources()
@@ -345,7 +393,7 @@ def initialize_scheduler():
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
hours=library_hours, minutes=0, seconds=0)
schedule_job(activity_pinger.check_server_response, 'Check server response',
schedule_job(activity_pinger.check_server_response, 'Check for server response',
hours=0, minutes=0, seconds=0)
else:
@@ -367,7 +415,7 @@ def initialize_scheduler():
response_seconds = CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS * CONFIG.WEBSOCKET_CONNECTION_TIMEOUT
response_seconds = 60 if response_seconds < 60 else response_seconds
schedule_job(activity_pinger.check_server_response, 'Check server response',
schedule_job(activity_pinger.check_server_response, 'Check for server response',
hours=0, minutes=0, seconds=response_seconds)
# Start scheduler
@@ -410,6 +458,7 @@ def start():
# Start background notification thread
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
notifiers.check_browser_enabled()
_STARTED = True
@@ -558,7 +607,7 @@ def dbcheck():
# poster_urls table :: This table keeps record of the notification poster urls
c_db.execute(
'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
@@ -1523,6 +1572,15 @@ def dbcheck():
'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.
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
if not result.fetchone():
@@ -1549,6 +1607,7 @@ def upgrade():
def shutdown(restart=False, update=False, checkout=False):
cherrypy.engine.exit()
SCHED.shutdown(wait=False)
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
# Stop the notification threads
for i in range(CONFIG.NOTIFICATION_THREADS):
@@ -1579,23 +1638,35 @@ def shutdown(restart=False, update=False, checkout=False):
if restart:
logger.info(u"Tautulli is restarting...")
exe = sys.executable
args = [exe, FULL_PATH]
args += ARGS
if '--nolaunch' not in args:
args += ['--nolaunch']
# os.execv fails with spaced names on Windows
# https://bugs.python.org/issue19066
# Separate out logger so we can shutdown logger after
if NOFORK:
logger.info('Running as service, not forking. Exiting...')
elif os.name == 'nt':
logger.info('Restarting Tautulli with %s', args)
subprocess.Popen(args, cwd=os.getcwd())
else:
logger.info('Restarting Tautulli with %s', args)
logger.shutdown()
# os.execv fails with spaced names on Windows
# https://bugs.python.org/issue19066
if NOFORK:
pass
elif os.name == 'nt':
subprocess.Popen(args, cwd=os.getcwd())
else:
os.execv(exe, args)
else:
logger.shutdown()
os._exit(0)

View File

@@ -54,7 +54,7 @@ class ActivityHandler(object):
def get_rating_key(self):
if self.is_valid_session():
return int(self.timeline['ratingKey'])
return self.timeline['ratingKey']
return None
@@ -65,6 +65,10 @@ class ActivityHandler(object):
if session_list:
for session in session_list['sessions']:
if int(session['session_key']) == self.get_session_key():
# Live sessions don't have rating keys in sessions
# Get it from the websocket data
if not session['rating_key']:
session['rating_key'] = self.get_rating_key()
return session
return None
@@ -93,14 +97,15 @@ class ActivityHandler(object):
% (str(session['session_key']), str(session['user_id']), session['username'],
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
self.update_db_session(session=session)
def on_stop(self, force_stop=False):
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
ap = activity_processor.ActivityProcessor()
@@ -117,17 +122,23 @@ class ActivityHandler(object):
# Retrieve the session data from our temp table
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
monitor_proc = activity_processor.ActivityProcessor()
monitor_proc.write_session_history(session=db_session)
row_id = monitor_proc.write_session_history(session=db_session)
# Remove the session from our temp session table
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
% (str(self.get_session_key()), str(self.get_rating_key())))
ap.delete_session(session_key=self.get_session_key())
delete_metadata_cache(self.get_session_key())
if row_id:
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
# Remove the session from our temp session table
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
% (str(self.get_session_key()), str(self.get_rating_key())))
ap.delete_session(row_id=row_id)
delete_metadata_cache(self.get_session_key())
else:
schedule_callback('session_key-{}'.format(self.get_session_key()), func=force_stop_stream,
args=[self.get_session_key()], seconds=30)
def on_pause(self, still_paused=False):
if self.is_valid_session():
@@ -148,7 +159,7 @@ class ActivityHandler(object):
db_session = ap.get_session_by_key(session_key=self.get_session_key())
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):
if self.is_valid_session():
@@ -167,7 +178,7 @@ class ActivityHandler(object):
# Retrieve the session data from our temp table
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):
if self.is_valid_session():
@@ -205,7 +216,7 @@ class ActivityHandler(object):
# Retrieve the session data from our temp table
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
def process(self):
@@ -220,7 +231,7 @@ class ActivityHandler(object):
if db_session:
# Re-schedule the callback to reset the 5 minutes timer
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_key = str(db_session['rating_key'])
@@ -245,9 +256,6 @@ class ActivityHandler(object):
elif this_state == 'stopped':
self.on_stop()
# Remove the callback if the stream is stopped
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
elif this_state == 'buffering':
self.on_buffer()
@@ -271,7 +279,8 @@ class ActivityHandler(object):
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
db_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):
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_watched'})
logger.debug(u"Tautulli ActivityHandler :: Session %s watched." % str(self.get_session_key()))
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_watched'})
else:
# We don't have this session in our table yet, start a new one.
@@ -280,7 +289,7 @@ class ActivityHandler(object):
# Schedule a callback to force stop a stale stream 5 minutes later
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):
@@ -324,6 +333,7 @@ class TimelineHandler(object):
9: 'album',
10: 'track'}
identifier = self.timeline.get('identifier')
state_type = self.timeline.get('state')
media_type = media_types.get(self.timeline.get('type'))
section_id = self.timeline.get('sectionID', 0)
@@ -332,6 +342,10 @@ class TimelineHandler(object):
media_state = self.timeline.get('mediaState')
queue_size = self.timeline.get('queueSize')
# Return if it is not a library event (i.e. DVR EPG event)
if identifier != 'com.plexapp.plugins.library':
return
# Add a new media item to the recently added queue
if media_type and section_id > 0 and \
((state_type == 0 and metadata_state == 'created')): # or \
@@ -358,7 +372,7 @@ class TimelineHandler(object):
% (title, str(rating_key), str(grandparent_rating_key)))
# 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)
elif media_type in ('season', 'album'):
@@ -374,7 +388,7 @@ class TimelineHandler(object):
% (title, str(rating_key), str(parent_rating_key)))
# 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)
else:
@@ -385,7 +399,7 @@ class TimelineHandler(object):
% (title, str(rating_key)))
# 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)
# A movie, show, or artist is done processing
@@ -415,7 +429,7 @@ def del_keys(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 remove_job:
ACTIVITY_SCHED.remove_job(id)
@@ -425,7 +439,7 @@ def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs):
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
elif not remove_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)))
@@ -433,13 +447,14 @@ def force_stop_stream(session_key):
ap = activity_processor.ActivityProcessor()
session = ap.get_session_by_key(session_key=session_key)
success = ap.write_session_history(session=session)
row_id = ap.write_session_history(session=session)
if success:
# If session is written to the databaase successfully, remove the session from the session table
if row_id:
# 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"
% (session['session_key'], session['rating_key']))
ap.delete_session(session_key=session_key)
ap.delete_session(row_id=row_id)
delete_metadata_cache(session_key)
else:
session['write_attempts'] += 1
@@ -451,7 +466,7 @@ def force_stop_stream(session_key):
ap.increment_write_attempts(session_key=session_key)
# 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)
else:
@@ -498,12 +513,12 @@ def on_created(rating_key, **kwargs):
if metadata:
notify = True
now = int(time.time())
if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
% str(rating_key))
notify = False
# now = int(time.time())
#
# if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
# logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
# % str(rating_key))
# notify = False
data_factory = datafactory.DataFactory()
if 'child_keys' not in kwargs:
@@ -532,7 +547,7 @@ def on_created(rating_key, **kwargs):
def delete_metadata_cache(session_key):
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:
logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
% (session_key, e))

View File

@@ -61,12 +61,12 @@ def check_active_sessions(ws_request=False):
if session['state'] == 'paused':
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':
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:
# 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 = ?',
[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:
# Subsequent buffer notifications after wait time
@@ -118,7 +118,7 @@ def check_active_sessions(ws_request=False):
'WHERE session_key = ? AND 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."
% (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'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
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:
# The user has stopped playing a stream
@@ -155,19 +155,18 @@ def check_active_sessions(ws_request=False):
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
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
success = monitor_process.write_session_history(session=stream)
if success:
row_id = monitor_process.write_session_history(session=stream)
if row_id:
# If session is written to the databaase successfully, remove the session from the session table
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
% (stream['session_key'], stream['rating_key']))
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
monitor_process.delete_session(row_id=row_id)
else:
stream['write_attempts'] += 1
@@ -175,18 +174,14 @@ def check_active_sessions(ws_request=False):
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
"Will try again on the next pass. Write attempt %s."
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
monitor_db.action('UPDATE sessions SET write_attempts = ? '
'WHERE session_key = ? AND rating_key = ?',
[stream['write_attempts'], stream['session_key'], stream['rating_key']])
monitor_process.increment_write_attempts(session_key=stream['session_key'])
else:
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
"Removing session from the database. Write attempt %s."
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
% (stream['session_key'], stream['rating_key']))
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
monitor_process.delete_session(session_key=stream['session_key'])
# Process the newly received session data
for session in media_container:
@@ -248,7 +243,7 @@ def check_recently_added():
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']))
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'})
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
else:
item = max(metadata, key=lambda x:x['added_at'])
@@ -266,7 +261,7 @@ def check_recently_added():
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
# 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():

View File

@@ -127,7 +127,7 @@ class ActivityProcessor(object):
if result == 'insert':
# Check if any notification agents have notifications enabled
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.
started = int(time.time())
@@ -155,7 +155,12 @@ class ActivityProcessor(object):
# Reload json from raw stream info
if session.get('raw_stream_info'):
session.update(json.loads(session['raw_stream_info']))
raw_stream_info = json.loads(session['raw_stream_info'])
# Don't overwrite id, session_key, stopped
raw_stream_info.pop('id', None)
raw_stream_info.pop('session_key', None)
raw_stream_info.pop('stopped', None)
session.update(raw_stream_info)
session = defaultdict(str, session)
@@ -177,6 +182,7 @@ class ActivityProcessor(object):
else:
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
return session['id']
if str(session['paused_counter']).isdigit():
real_play_time = stopped - session['started'] - int(session['paused_counter'])
@@ -229,7 +235,8 @@ class ActivityProcessor(object):
## TODO: Fix media info from imports. Temporary media info from import session.
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}
values = {'started': session['started'],
'stopped': stopped,
@@ -254,7 +261,8 @@ class ActivityProcessor(object):
'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)
# Check if we should group the session, select the last two rows from the user
@@ -284,7 +292,7 @@ class ActivityProcessor(object):
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
if prev_session == new_session == None:
if prev_session is None and new_session is None:
args = [last_id, last_id]
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
args = [prev_session['reference_id'], new_session['id']]
@@ -298,7 +306,8 @@ class ActivityProcessor(object):
# 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}
values = {'rating_key': session['rating_key'],
'video_decision': session['video_decision'],
@@ -365,7 +374,8 @@ class ActivityProcessor(object):
'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)
# Write the session_history_metadata table
@@ -375,7 +385,8 @@ class ActivityProcessor(object):
genres = ";".join(metadata['genres'])
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}
values = {'rating_key': session['rating_key'],
'parent_rating_key': session['parent_rating_key'],
@@ -411,11 +422,12 @@ class ActivityProcessor(object):
'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)
# Return true when the session is successfully written to the database
return True
# Return the session row id when the session is successfully written to the database
return session['id']
def get_sessions(self, user_id=None, ip_address=None):
query = 'SELECT * FROM sessions'
@@ -456,9 +468,11 @@ class ActivityProcessor(object):
return None
def delete_session(self, session_key=None):
def delete_session(self, session_key=None, row_id=None):
if str(session_key).isdigit():
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
elif str(row_id).isdigit():
self.db.action('DELETE FROM sessions WHERE id = ?', [row_id])
def set_session_last_paused(self, session_key=None, timestamp=None):
if str(session_key).isdigit():

View File

@@ -174,11 +174,11 @@ HW_ENCODERS = [
SCHEDULER_LIST = [
'Check GitHub for updates',
'Check for server response',
'Check for active sessions',
'Check for recently added items',
'Check for Plex updates',
'Check for Plex remote access',
'Check server response',
'Refresh users list',
'Refresh libraries list',
'Refresh Plex server URLs',
@@ -279,16 +279,22 @@ NOTIFICATION_PARAMETERS = [
{
'category': 'Global',
'parameters': [
{'name': 'Tautulli Version', 'type': 'str', 'value': 'plexpy_version', 'description': 'The current version of Tautulli.'},
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'plexpy_branch', 'description': 'The current git branch of Tautulli.'},
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'plexpy_commit', 'description': 'The current git commit hash of Tautulli.'},
{'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', 'description': 'The current version of Tautulli.'},
{'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', 'description': 'The current git remote of Tautulli.'},
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', 'description': 'The current git branch of Tautulli.'},
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', 'description': 'The current git commit hash of Tautulli.'},
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
{'name': 'Server Uptime', 'type': 'str', 'value': 'server_uptime', 'description': 'The uptime (in days, hours, mins, secs) of your Plex Server.'},
{'name': 'Server IP', 'type': 'str', 'value': 'server_ip', 'description': 'The connection IP address for your Plex Server.'},
{'name': 'Server Port', 'type': 'int', 'value': 'server_port', 'description': 'The connection port for your Plex Server.'},
{'name': 'Server URL', 'type': 'str', 'value': 'server_url', 'description': 'The connection URL for your Plex Server.'},
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', 'description': 'The platform of your Plex Server.'},
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
{'name': 'Datestamp', 'type': 'int', 'value': 'datestamp', 'description': 'The date (in date format) the notification was triggered.'},
{'name': 'Timestamp', 'type': 'int', 'value': 'timestamp', 'description': 'The time (in time format) the notification was triggered.'},
]
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification was triggered.'},
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification was triggered.'},
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification was triggered.'},
]
},
{
'category': 'Stream Details',
@@ -394,10 +400,12 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
{'name': 'Director', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
{'name': 'Writer', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
{'name': 'Actor', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
{'name': 'Genre', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
{'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
{'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
{'name': 'Collections', 'type': 'str', 'value': 'collections', 'description': 'A list of collections for the item.'},
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
@@ -477,12 +485,12 @@ NOTIFICATION_PARAMETERS = [
{
'category': 'Tautulli Update Available',
'parameters': [
{'name': 'Plexpy Update Version', 'type': 'int', 'value': 'plexpy_update_version', 'description': 'The available update version for Tautulli.'},
{'name': 'Plexpy Update Tar', 'type': 'int', 'value': 'plexpy_update_tar', 'description': 'The tar download URL for the available update.'},
{'name': 'Plexpy Update Zip', 'type': 'int', 'value': 'plexpy_update_zip', 'description': 'The zip download URL for the available update.'},
{'name': 'Plexpy Update Commit', 'type': 'int', 'value': 'plexpy_update_commit', 'description': 'The commit hash for the available update.'},
{'name': 'Plexpy Update Behind', 'type': 'int', 'value': 'plexpy_update_behind', 'description': 'The number of commits behind for the available update.'},
{'name': 'Plexpy Update Changelog', 'type': 'int', 'value': 'plexpy_update_changelog', 'description': 'The changelog for the available update.'},
{'name': 'Tautulli Update Version', 'type': 'int', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'},
{'name': 'Tautulli Update Tar', 'type': 'int', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'},
{'name': 'Tautulli Update Zip', 'type': 'int', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'},
{'name': 'Tautulli Update Commit', 'type': 'int', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'},
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'},
{'name': 'Tautulli Update Changelog', 'type': 'int', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'},
]
},
]

View File

@@ -61,7 +61,7 @@ _CONFIG_DEFINITIONS = {
'PMS_PLEXPASS': (int, 'PMS', 0),
'PMS_PLATFORM': (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_BUILD': (str, 'PMS', ''),
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
@@ -176,13 +176,13 @@ _CONFIG_DEFINITIONS = {
'GIT_PATH': (str, 'General', ''),
'GIT_REMOTE': (str, 'General', 'origin'),
'GIT_TOKEN': (str, 'General', ''),
'GIT_USER': (str, 'General', 'JonnyWong16'),
'GIT_REPO': (str, 'General', 'plexpy'),
'GIT_USER': (str, 'General', 'Tautulli'),
'GIT_REPO': (str, 'General', 'Tautulli'),
'GRAPH_TYPE': (str, 'General', 'plays'),
'GRAPH_DAYS': (int, 'General', 30),
'GRAPH_MONTHS': (int, 'General', 12),
'GRAPH_TAB': (str, 'General', 'tabs-1'),
'GROUP_HISTORY_TABLES': (int, 'General', 0),
'GROUP_HISTORY_TABLES': (int, 'General', 1),
'GROWL_ENABLED': (int, 'Growl', 0),
'GROWL_HOST': (str, 'Growl', ''),
'GROWL_PASSWORD': (str, 'Growl', ''),
@@ -480,6 +480,7 @@ _CONFIG_DEFINITIONS = {
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
'REMOTE_ACCESS_PING_THRESHOLD': (int, 'Advanced', 3),
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
'SHOW_ADVANCED_SETTINGS': (int, 'General', 0),
'SLACK_ENABLED': (int, 'Slack', 0),
'SLACK_HOOK': (str, 'Slack', ''),
'SLACK_CHANNEL': (str, 'Slack', ''),
@@ -876,3 +877,15 @@ class Config(object):
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
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

@@ -1107,6 +1107,7 @@ class DataFactory(object):
def get_poster_info(self, rating_key='', metadata=None):
monitor_db = database.MonitorDatabase()
poster_key = ''
if str(rating_key).isdigit():
poster_key = rating_key
elif metadata:
@@ -1118,6 +1119,7 @@ class DataFactory(object):
poster_key = metadata['parent_rating_key']
poster_info = {}
if poster_key:
try:
query = 'SELECT poster_title, poster_url FROM poster_urls ' \
@@ -1128,14 +1130,15 @@ class DataFactory(object):
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()
if str(rating_key).isdigit():
keys = {'rating_key': int(rating_key)}
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)
@@ -1143,10 +1146,62 @@ class DataFactory(object):
monitor_db = database.MonitorDatabase()
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])
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=''):
monitor_db = database.MonitorDatabase()

View File

@@ -698,6 +698,10 @@ class Graphs(object):
series_3 = []
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'])
series_1.append(item['dp_count'])
series_2.append(item['ds_count'])
@@ -729,16 +733,18 @@ class Graphs(object):
try:
if y_axis == 'plays':
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.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 <= 576 THEN "576" ' \
'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 <= 1440 THEN "QHD" ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
'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" ' \
'THEN 1 ELSE 0 END) AS dp_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
@@ -758,16 +764,18 @@ class Graphs(object):
result = monitor_db.select(query)
else:
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.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 <= 576 THEN "576" ' \
'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 <= 1440 THEN "QHD" ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
'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" ' \
'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, ' \
@@ -799,6 +807,10 @@ class Graphs(object):
series_3 = []
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'])
series_1.append(item['dp_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))
def uploadToImgur(imgPath, imgTitle=''):
def upload_to_imgur(imgPath, imgTitle=''):
""" Uploads an image to Imgur """
client_id = plexpy.CONFIG.IMGUR_CLIENT_ID
img_url = ''
img_url = delete_hash = ''
if not client_id:
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:
with open(imgPath, 'rb') as imgFile:
img = imgFile.read()
except IOError as 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}
data = {'type': 'base64',
@@ -703,13 +703,15 @@ def uploadToImgur(imgPath, imgTitle=''):
data['title'] = imgTitle.encode('utf-8')
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:
t = '\'' + imgTitle + '\' ' if imgTitle else ''
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:
if 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:
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):
"""

View File

@@ -65,7 +65,7 @@ class HTTPHandler(object):
Output: list
"""
self.uri = uri
self.uri = uri.encode('utf-8')
self.request_type = request_type.upper()
self.output_format = output_format.lower()
self.return_type = return_type

View File

@@ -544,19 +544,19 @@ class Libraries(object):
filtered_count = len(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']
for order in reversed(sort_order):
sort_key = json_data['columns'][int(order['column'])]['data']
reverse = True if order['dir'] == 'desc' else False
if rating_key and sort_key == 'sort_title':
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)
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)
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])

View File

@@ -306,6 +306,11 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
# Monkey patch the run() by monkey patching the __init__ method
threading.Thread.__init__ = new_init
def shutdown():
logging.shutdown()
# Expose logger methods
# Main Tautulli logger
info = logger.info

View File

@@ -82,11 +82,18 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
# Check if any notification agents have notifications enabled for the action
notifiers_enabled = notifiers.get_notifiers(notify_action=notify_action)
# Check if the watched notifications has already been sent
if stream_data and notify_action == 'on_watched':
watched_notifiers = [d['notifier_id'] for d in get_notify_state(session=stream_data)]
notifiers_enabled = [n for n in notifiers_enabled if n['id'] not in watched_notifiers]
if notifiers_enabled and not manual_trigger:
# Check if notification conditions are satisfied
conditions = notify_conditions(notify_action=notify_action,
stream_data=stream_data,
timeline_data=timeline_data)
else:
conditions = True
if notifiers_enabled and (manual_trigger or conditions):
if stream_data or timeline_data:
@@ -122,8 +129,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
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, 'notify_action': 'on_newdevice'})
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
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):
@@ -277,13 +284,13 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
evaluated_conditions.append(any(c in parameter_value for c in values))
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':
evaluated_conditions.append(any(parameter_value == c for c in values))
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':
evaluated_conditions.append(parameter_value.startswith(tuple(values)))
@@ -318,13 +325,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):
# 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)
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
@@ -435,20 +435,6 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
if server_times:
updated_at = server_times['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
else:
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
# Get metadata for the item
if session:
rating_key = session['rating_key']
@@ -523,10 +509,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
remaining_duration = duration - view_offset
# Build Plex URL
if notify_params['media_type'] == 'track':
plex_web_rating_key = notify_params['parent_rating_key']
else:
plex_web_rating_key = notify_params['rating_key']
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
web_url=plexpy.CONFIG.PMS_WEB_URL,
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
rating_key=rating_key)
rating_key=plex_web_rating_key)
# Get media IDs from guid and build URLs
if 'imdb://' in notify_params['guid']:
@@ -540,7 +531,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'
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['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
@@ -551,7 +542,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'
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['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
@@ -571,7 +562,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']
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'),
imdb_id=notify_params.get('imdb_id'))
notify_params.update(themoviedb_info)
@@ -579,7 +577,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
# Get TVmaze info (for tv shows only)
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')):
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'),
imdb_id=notify_params.get('imdb_id'))
notify_params.update(tvmaze_info)
@@ -655,15 +660,21 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
available_params = {
# Global paramaters
'plexpy_version': common.VERSION_NUMBER,
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
'plexpy_commit': plexpy.CURRENT_VERSION,
'server_name': server_name,
'server_uptime': server_uptime,
'server_version': server_times.get('version', ''),
'tautulli_version': common.VERSION_NUMBER,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'tautulli_commit': plexpy.CURRENT_VERSION,
'server_name': plexpy.CONFIG.PMS_NAME,
'server_ip': plexpy.CONFIG.PMS_IP,
'server_port': plexpy.CONFIG.PMS_PORT,
'server_url': plexpy.CONFIG.PMS_URL,
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
'server_version': plexpy.CONFIG.PMS_VERSION,
'action': notify_action.lstrip('on_'),
'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(time_format),
'unixtime': int(time.time()),
# Stream parameters
'streams': stream_count,
'user_streams': user_stream_count,
@@ -772,6 +783,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'writers': ', '.join(notify_params['writers']),
'actors': ', '.join(notify_params['actors']),
'genres': ', '.join(notify_params['genres']),
'labels': ', '.join(notify_params['labels']),
'collections': ', '.join(notify_params['collections']),
'summary': notify_params['summary'],
'tagline': notify_params['tagline'],
'rating': notify_params['rating'],
@@ -840,40 +853,34 @@ def build_server_notify_params(notify_action=None, **kwargs):
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
if server_times:
updated_at = server_times['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
else:
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
available_params = {
# Global paramaters
'plexpy_version': common.VERSION_NUMBER,
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
'plexpy_commit': plexpy.CURRENT_VERSION,
'server_name': server_name,
'server_uptime': server_uptime,
'server_version': server_times.get('version', ''),
'tautulli_version': common.VERSION_NUMBER,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'tautulli_commit': plexpy.CURRENT_VERSION,
'server_name': plexpy.CONFIG.PMS_NAME,
'server_ip': plexpy.CONFIG.PMS_IP,
'server_port': plexpy.CONFIG.PMS_PORT,
'server_url': plexpy.CONFIG.PMS_URL,
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
'server_version': plexpy.CONFIG.PMS_VERSION,
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
'action': notify_action.lstrip('on_'),
'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(time_format),
'unixtime': int(time.time()),
# Plex Media Server update parameters
'update_version': pms_download_info['version'],
'update_url': pms_download_info['download_url'],
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
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_distro': pms_download_info['distro'],
'update_distro_build': pms_download_info['build'],
@@ -882,12 +889,12 @@ def build_server_notify_params(notify_action=None, **kwargs):
'update_changelog_added': pms_download_info['changelog_added'],
'update_changelog_fixed': pms_download_info['changelog_fixed'],
# Tautulli update parameters
'plexpy_update_version': plexpy_download_info['tag_name'],
'plexpy_update_tar': plexpy_download_info['tarball_url'],
'plexpy_update_zip': plexpy_download_info['zipball_url'],
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''),
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''),
'plexpy_update_changelog': plexpy_download_info['body']
'tautulli_update_version': plexpy_download_info['tag_name'],
'tautulli_update_tar': plexpy_download_info['tarball_url'],
'tautulli_update_zip': plexpy_download_info['zipball_url'],
'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),
'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''),
'tautulli_update_changelog': plexpy_download_info['body']
}
return available_params
@@ -987,8 +994,8 @@ def strip_tag(data, agent_id=None):
'font': ['color']}
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
elif agent_id == 10:
# Don't remove tags for email
elif agent_id in (10, 14, 20):
# Don't remove tags for Email, Slack, and Discord
return data
elif agent_id == 13:
@@ -1046,14 +1053,17 @@ def get_poster_info(poster_thumb, poster_key, poster_title):
raise Exception(u'PMS image request failed')
# 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:
# Create poster info
poster_info = {'poster_title': poster_title, 'poster_url': poster_url}
# 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
os.remove(poster_file)
@@ -1203,6 +1213,17 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
if response and not err_msg:
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:
if err_msg:

View File

@@ -64,6 +64,9 @@ import users
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
BROWSER_NOTIFIERS = {}
AGENT_IDS = {'growl': 0,
'prowl': 1,
'xbmc': 2,
@@ -87,7 +90,8 @@ AGENT_IDS = {'growl': 0,
'discord': 20,
'androidapp': 21,
'groupme': 22,
'mqtt': 23
'mqtt': 23,
'zapier': 24
}
@@ -183,6 +187,10 @@ def available_notification_agents():
{'label': 'XBMC',
'name': 'xbmc',
'id': AGENT_IDS['xbmc']
},
{'label': 'Zapier',
'name': 'zapier',
'id': AGENT_IDS['zapier']
}
]
@@ -313,7 +321,7 @@ def available_notification_actions():
'name': 'on_plexpyupdate',
'description': 'Trigger a notification when an update for the Tautulli is available.',
'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',
'media_types': ('server',)
}
@@ -374,6 +382,8 @@ def get_agent_class(agent_id=None, config=None):
return GROUPME(config=config)
elif agent_id == 23:
return MQTT(config=config)
elif agent_id == 24:
return ZAPIER(config=config)
else:
return Notifier(config=config)
else:
@@ -551,6 +561,10 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
blacklist_logger()
if agent['name'] == 'browser':
check_browser_enabled()
return True
except Exception as e:
logger.warn(u"Tautulli Notifiers :: Unable to update notification agent: %s." % e)
@@ -645,13 +659,28 @@ class PrettyMetadata(object):
provider_name = 'Trakt.tv'
elif provider == 'lastfm':
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
def get_provider_link(self, provider=None):
provider_link = ''
if provider == 'plexweb':
provider_link = self.get_plex_url()
else:
elif provider:
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
def get_caption(self, provider):
@@ -867,7 +896,8 @@ class ANDROIDAPP(Notifier):
'The content of your notifications will be sent unencrypted!</strong><br>' \
'Please install the library to encrypt the notification contents. ' \
'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'
})
else:
@@ -994,40 +1024,15 @@ class BROWSER(Notifier):
Browser notifications
"""
NAME = 'Browser'
_DEFAULT_CONFIG = {'enabled': 0,
'auto_hide_delay': 5
_DEFAULT_CONFIG = {'auto_hide_delay': 5
}
def agent_notify(self, subject='', body='', action='', **kwargs):
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
return True
def get_notifications(self):
if not self.config['enabled']:
return
db = database.MonitorDatabase()
result = db.select('SELECT subject_text, body_text FROM notify_log '
'WHERE agent_id = 17 AND timestamp >= ? ',
args=[time.time() - 3])
notifications = []
for item in result:
notification = {'subject_text': item['subject_text'],
'body_text': item['body_text'],
'delay': self.config['auto_hide_delay']}
notifications.append(notification)
return {'notifications': notifications}
def return_config_options(self):
config_option = [{'label': 'Enable Browser Notifications',
'value': self.config['enabled'],
'name': 'browser_enabled',
'description': 'Enable to display desktop notifications from your browser.',
'input_type': 'checkbox'
},
{'label': 'Allow Notifications',
config_option = [{'label': 'Allow Notifications',
'value': 'Allow Notifications',
'name': 'browser_allow_browser',
'description': 'Click to allow browser notifications. You must click this button for each browser.',
@@ -1258,13 +1263,13 @@ class EMAIL(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs):
if self.config['html_support']:
body = body.replace('\n', '<br />')
msg = MIMEMultipart('alternative')
msg.attach(MIMEText(bleach.clean(body, strip=True), 'plain', 'utf-8'))
msg.attach(MIMEText(body, 'html', 'utf-8'))
else:
msg = MIMEText(body, 'plain', 'utf-8')
msg['Date'] = email.utils.formatdate(localtime=True)
msg['Subject'] = subject
msg['From'] = email.utils.formataddr((self.config['from_name'], self.config['from']))
msg['To'] = ','.join(self.config['to'])
@@ -1293,8 +1298,25 @@ class EMAIL(Notifier):
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
return False
def get_user_emails(self):
emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']}
user_emails_to = {v: '' for v in self.config['to']}
user_emails_cc = {v: '' for v in self.config['cc']}
user_emails_bcc = {v: '' for v in self.config['bcc']}
user_emails_to.update(emails)
user_emails_cc.update(emails)
user_emails_bcc.update(emails)
user_emails_to = [{'value': k, 'text': v} for k, v in user_emails_to.iteritems()]
user_emails_cc = [{'value': k, 'text': v} for k, v in user_emails_cc.iteritems()]
user_emails_bcc = [{'value': k, 'text': v} for k, v in user_emails_bcc.iteritems()]
return user_emails_to, user_emails_cc, user_emails_bcc
def return_config_options(self):
user_emails = {} # User selection set with selectize options
user_emails_to, user_emails_cc, user_emails_bcc = self.get_user_emails()
config_option = [{'label': 'From Name',
'value': self.config['from_name'],
@@ -1312,22 +1334,22 @@ class EMAIL(Notifier):
'value': self.config['to'],
'name': 'email_to',
'description': 'The email address(es) of the recipients.',
'input_type': 'select',
'select_options': user_emails
'input_type': 'selectize',
'select_options': user_emails_to
},
{'label': 'CC',
'value': self.config['cc'],
'name': 'email_cc',
'description': 'The email address(es) to CC.',
'input_type': 'select',
'select_options': user_emails
'input_type': 'selectize',
'select_options': user_emails_cc
},
{'label': 'BCC',
'value': self.config['bcc'],
'name': 'email_bcc',
'description': 'The email address(es) to BCC.',
'input_type': 'select',
'select_options': user_emails
'input_type': 'selectize',
'select_options': user_emails_bcc
},
{'label': 'SMTP Server',
'value': self.config['smtp_server'],
@@ -1362,8 +1384,7 @@ class EMAIL(Notifier):
{'label': 'Enable HTML Support',
'value': self.config['html_support'],
'name': 'email_html_support',
'description': 'Style your messages using HTML tags. '
'Line breaks (&lt;br&gt;) will be inserted automatically.',
'description': 'Style your messages using HTML tags.',
'input_type': 'checkbox'
}
]
@@ -1921,23 +1942,24 @@ class IFTTT(Notifier):
headers=headers, json=data)
def return_config_options(self):
config_option = [{'label': 'Ifttt Maker Channel Key',
config_option = [{'label': 'IFTTT Webhook Key',
'value': self.config['key'],
'name': 'ifttt_key',
'description': 'Your Ifttt key. You can get a key from'
' <a href="' + helpers.anon_url('https://ifttt.com/maker') + '" target="_blank">here</a>.',
'description': 'Your IFTTT webhook key. You can get a key from'
' <a href="' + helpers.anon_url('https://ifttt.com/maker_webhooks') + '" target="_blank">here</a>.',
'input_type': 'text'
},
{'label': 'Ifttt Event',
{'label': 'IFTTT Event',
'value': self.config['event'],
'name': 'ifttt_event',
'description': 'The Ifttt maker event to fire. You can include'
' the {action} to be substituted with the action name.'
'description': 'The IFTTT maker event to fire. You can include'
' <span class="inline-pre">{action}</span>'
' to be substituted with the action name.'
' 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'
}
]
return config_option
@@ -2074,7 +2096,7 @@ class JOIN(Notifier):
{'label': 'Movie Link Source',
'value': self.config['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.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -2082,7 +2104,7 @@ class JOIN(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['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.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -2090,7 +2112,7 @@ class JOIN(Notifier):
{'label': 'Music Link Source',
'value': self.config['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',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -2716,7 +2738,7 @@ class PUSHOVER(Notifier):
{'label': 'Movie Link Source',
'value': self.config['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.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -2724,7 +2746,7 @@ class PUSHOVER(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['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.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -2732,7 +2754,7 @@ class PUSHOVER(Notifier):
{'label': 'Music Link Source',
'value': self.config['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',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -3405,6 +3427,104 @@ class XBMC(Notifier):
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():
logger.info(u"Tautulli Notifiers :: Upgrading to new notification system...")
@@ -3527,3 +3647,27 @@ def upgrade_config_to_db():
notifier_id = add_notifier_config(agent_id=agent_id)
set_notifier_config(notifier_id=notifier_id, agent_id=agent_id, **notifier_config)
def check_browser_enabled():
global BROWSER_NOTIFIERS
BROWSER_NOTIFIERS = {}
for n in get_notifiers():
if n['agent_id'] == 17 and n['active']:
notifier_config = get_notifier_config(n['id'])
BROWSER_NOTIFIERS[n['id']] = notifier_config['config']['auto_hide_delay']
def get_browser_notifications():
db = database.MonitorDatabase()
result = db.select('SELECT notifier_id, subject_text, body_text FROM notify_log '
'WHERE agent_id = 17 AND timestamp >= ? ',
args=[time.time() - 5])
notifications = []
for item in result:
notification = {'subject_text': item['subject_text'],
'body_text': item['body_text'],
'delay': BROWSER_NOTIFIERS.get(item['notifier_id'], 5)}
notifications.append(notification)
return {'notifications': notifications}

View File

@@ -376,9 +376,19 @@ class PlexTV(object):
def get_synced_items(self, machine_id=None, client_id_filter=None, user_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
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')
user_data = users.Users()
@@ -418,7 +428,7 @@ class PlexTV(object):
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
# 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
for synced in a.getElementsByTagName('SyncItems'):
@@ -432,7 +442,7 @@ class PlexTV(object):
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
# 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
sync_id = helpers.get_xml_attr(item, 'id')
@@ -461,12 +471,13 @@ class PlexTV(object):
status_item_downloaded_count, status_item_count)
for settings in item.getElementsByTagName('MediaSettings'):
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
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_bitrate = helpers.get_xml_attr(settings, 'maxVideoBitrate')
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
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),
"platform": helpers.sanitize(device_platform),
@@ -483,7 +494,8 @@ class PlexTV(object):
"item_complete_count": status_item_complete_count,
"item_downloaded_count": status_item_downloaded_count,
"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,
"video_quality": settings_video_quality,
"total_size": status_total_size,
@@ -641,10 +653,14 @@ class PlexTV(object):
def get_plex_downloads(self):
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)
plex_downloads = self.get_plextv_downloads(plexpass=(plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass'))
pms_connect = pmsconnect.PmsConnect()
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:
available_downloads = json.loads(plex_downloads)

View File

@@ -533,11 +533,11 @@ class PmsConnect(object):
metadata = {}
if cache_key:
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
try:
with open(in_file_path, 'r') as inFile:
metadata = json.load(inFile)
except IOError as e:
except (IOError, ValueError) as e:
pass
if metadata:
@@ -559,27 +559,32 @@ class PmsConnect(object):
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') != '1':
if a.getAttribute('size') == '0':
return metadata
if a.getElementsByTagName('Directory'):
metadata_main = a.getElementsByTagName('Directory')[0]
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
if metadata_type == 'photo':
metadata_type = 'photo_album'
metadata_main_list = a.getElementsByTagName('Directory')
elif a.getElementsByTagName('Video'):
metadata_main = a.getElementsByTagName('Video')[0]
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
metadata_main_list = a.getElementsByTagName('Video')
elif a.getElementsByTagName('Track'):
metadata_main = a.getElementsByTagName('Track')[0]
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
metadata_main_list = a.getElementsByTagName('Track')
elif a.getElementsByTagName('Photo'):
metadata_main = a.getElementsByTagName('Photo')[0]
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
metadata_main_list = a.getElementsByTagName('Photo')
else:
logger.debug(u"Tautulli Pmsconnect :: Metadata failed")
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')
library_name = helpers.get_xml_attr(a, 'librarySectionTitle')
@@ -588,6 +593,7 @@ class PmsConnect(object):
actors = []
genres = []
labels = []
collections = []
if metadata_main.getElementsByTagName('Director'):
for director in metadata_main.getElementsByTagName('Director'):
@@ -609,6 +615,10 @@ class PmsConnect(object):
for label in metadata_main.getElementsByTagName('Label'):
labels.append(helpers.get_xml_attr(label, 'tag'))
if metadata_main.getElementsByTagName('Collection'):
for collection in metadata_main.getElementsByTagName('Collection'):
collections.append(helpers.get_xml_attr(collection, 'tag'))
if metadata_type == 'movie':
metadata = {'media_type': metadata_type,
'section_id': section_id,
@@ -646,6 +656,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
@@ -686,6 +697,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
@@ -728,6 +740,7 @@ class PmsConnect(object):
'actors': show_details['actors'],
'genres': show_details['genres'],
'labels': show_details['labels'],
'collections': show_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
}
@@ -771,6 +784,7 @@ class PmsConnect(object):
'actors': show_details['actors'],
'genres': show_details['genres'],
'labels': show_details['labels'],
'collections': show_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
}
@@ -812,6 +826,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
@@ -854,6 +869,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
}
@@ -897,6 +913,7 @@ class PmsConnect(object):
'actors': actors,
'genres': album_details['genres'],
'labels': album_details['labels'],
'collections': album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
}
@@ -938,6 +955,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
@@ -980,6 +998,7 @@ class PmsConnect(object):
'actors': actors,
'genres': photo_album_details['genres'],
'labels': photo_album_details['labels'],
'collections': photo_album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
}
@@ -1025,6 +1044,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
@@ -1065,6 +1085,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
@@ -1158,11 +1179,11 @@ class PmsConnect(object):
if cache_key:
metadata['_cache_time'] = int(time.time())
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
try:
with open(out_file_path, 'w') as outFile:
json.dump(metadata, outFile)
except IOError as e:
except (IOError, ValueError) as e:
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
% (cache_key, e))
@@ -1370,7 +1391,7 @@ class PmsConnect(object):
else:
session_details = {'session_id': '',
'bandwidth': '',
'location': 'Unknown'
'location': 'wan' if player_details['local'] == '0' else 'lan'
}
# Get the transcode details
@@ -1443,16 +1464,24 @@ class PmsConnect(object):
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':
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'],
rating_key_filter=rating_key)
rating_key_filter=[rating_key, parent_rating_key, grandparent_rating_key])
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_head = synced_xml.getElementsByTagName('MediaContainer')
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'):
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
if sync_id:
@@ -1586,6 +1615,7 @@ class PmsConnect(object):
channel_stream = 1
clip_media = session.getElementsByTagName('Media')[0]
clip_part = clip_media.getElementsByTagName('Part')[0]
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
metadata_details = {'media_type': media_type,
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
@@ -1624,7 +1654,8 @@ class PmsConnect(object):
'genres': [],
'labels': [],
'full_title': helpers.get_xml_attr(session, 'title'),
'container': helpers.get_xml_attr(clip_media, 'container'),
'container': helpers.get_xml_attr(clip_media, 'container') \
or helpers.get_xml_attr(clip_part, 'container'),
'height': helpers.get_xml_attr(clip_media, 'height'),
'width': helpers.get_xml_attr(clip_media, 'width'),
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
@@ -1633,7 +1664,8 @@ class PmsConnect(object):
'audio_channels': audio_channels,
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
'channel_title': helpers.get_xml_attr(session, 'sourceTitle')
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
'live': int(helpers.get_xml_attr(session, 'live') == '1')
}
else:
channel_stream = 0
@@ -1642,7 +1674,7 @@ class PmsConnect(object):
part_id = helpers.get_xml_attr(stream_media_parts_info, '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:
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
@@ -1699,51 +1731,72 @@ class PmsConnect(object):
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
# Overrides for live sessions
if metadata_details.get('live') and transcode_decision == 'transcode':
stream_details['stream_container_decision'] = 'transcode'
stream_details['stream_container'] = transcode_details['transcode_container']
video_details['stream_video_decision'] = transcode_details['video_decision']
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
stream_details['stream_video_resolution'] = metadata_details['video_resolution']
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
stream_details['stream_audio_channels'] = transcode_details['transcode_audio_channels']
stream_details['stream_audio_channel_layout'] = common.AUDIO_CHANNELS.get(
transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels'])
# Get the quality profile
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
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:
if sync_id:
quality_profile = 'Original'
if sync_id:
synced_item_bitrate = helpers.cast_to_int(synced_item_details['video_bitrate'])
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]
except ValueError:
synced_version_profile = 'Original'
else:
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']:
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:
optimized_version_profile = ''
elif media_type == 'track' and 'stream_bitrate' in stream_details:
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:
if sync_id:
quality_profile = 'Original'
if sync_id:
synced_item_bitrate = helpers.cast_to_int(synced_item_details['audio_bitrate'])
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]
except ValueError:
synced_version_profile = 'Original'
else:
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 = ''
elif media_type == 'photo':
@@ -2122,8 +2175,12 @@ class PmsConnect(object):
item_main += a.getElementsByTagName('Photo')
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'),
'media_type': helpers.get_xml_attr(item, 'type'),
'media_type': media_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
@@ -2533,4 +2590,15 @@ class PmsConnect(object):
version = identity.get('version', plexpy.CONFIG.PMS_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

@@ -597,7 +597,7 @@ class Users(object):
for item in result:
user = {'user_id': item['user_id'],
'username': item['username'],
'friendly_name': item['friendly_name'],
'friendly_name': item['friendly_name'] or item['username'],
'email': item['email']
}
users.append(user)

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.13-beta"
PLEXPY_RELEASE_VERSION = "v2.0.18-beta"

View File

@@ -136,7 +136,9 @@ def checkGithub(auto_update=False):
# Get the latest version available 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
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
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
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:
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)
if releases is None:
@@ -298,14 +303,14 @@ def checkout_git_branch():
logger.info('Output: ' + str(output))
def read_changelog(latest_only=False):
def read_changelog(latest_only=False, since_prev_release=False):
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
if not os.path.isfile(changelog_file):
return '<h4>Missing changelog file</h4>'
try:
output = ''
output = ['']
prev_level = 0
latest_version_found = False
@@ -329,27 +334,34 @@ def read_changelog(latest_only=False):
break
elif latest_only:
latest_version_found = True
# Add a space to the end of the release to match tags
elif since_prev_release and str(plexpy.PREV_RELEASE) + ' ' in header_text:
break
output += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
elif line_list_match:
line_level = len(line_list_match.group(1)) / 2
line_text = line_list_match.group(2)
if line_level > prev_level:
output += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
output[-1] += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
elif line_level < prev_level:
output += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
output[-1] += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
else:
output += '<li>' + line_text + '</li>'
output[-1] += '<li>' + line_text + '</li>'
prev_level = line_level
elif line.strip() == '' and prev_level:
output += '</ul>' * (prev_level)
output[-1] += '</ul>' * (prev_level)
output.append('')
prev_level = 0
return output
if since_prev_release:
output.reverse()
return ''.join(output)
except IOError as e:
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)

View File

@@ -180,7 +180,7 @@ def process(opcode, data):
info = json.loads(data)
except Exception as e:
logger.warn(u"Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
logger.debug(data)
logger.websocket_error(data)
return False
info = info.get('NotificationContainer', info)

View File

@@ -220,7 +220,7 @@ class AuthController(object):
# Save login to the database
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')
Users().set_user_login(user_id=user_id,

View File

@@ -62,7 +62,7 @@ def serve_template(templatename, **kwargs):
http_root = plexpy.HTTP_ROOT
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()
@@ -512,8 +512,6 @@ class WebInterface(object):
Optional parameters:
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
Returns:
@@ -624,7 +622,7 @@ class WebInterface(object):
Optional parameters:
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",
"file_size", "last_played", "play_count"
order_dir (str): "desc" or "asc"
@@ -674,7 +672,7 @@ class WebInterface(object):
if not kwargs.get('json_data'):
# Alias 'title' to 'sort_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
dt_columns = [("added_at", True, False),
@@ -981,7 +979,7 @@ class WebInterface(object):
else:
return {'message': 'no data received'}
else:
return {'message': 'Cannot refresh library while getting file sizes.'}
return {'message': 'Cannot delete media info cache while getting file sizes.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -1130,9 +1128,8 @@ class WebInterface(object):
Optional paramters:
friendly_name(str): The friendly name of the user
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
allow_guest (int): 0 or 1
Returns:
None
@@ -2201,9 +2198,8 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
def get_sync(self, machine_id=None, user_id=None, **kwargs):
if not machine_id:
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
if user_id == 'null':
user_id = None
plex_tv = plextv.PlexTV()
result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id)
@@ -2612,7 +2608,8 @@ class WebInterface(object):
"tv_watched_percent": plexpy.CONFIG.TV_WATCHED_PERCENT,
"music_watched_percent": plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
"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)
@@ -2798,12 +2795,16 @@ class WebInterface(object):
def get_server_update_params(self, **kwargs):
plex_tv = plextv.PlexTV()
plexpass = plex_tv.get_plexpass_status()
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
return {'plexpass': plexpass,
'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get(
plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM),
'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL,
'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.tools.json_out()
@@ -3011,12 +3012,14 @@ class WebInterface(object):
Pass all the config options for the agent with the agent prefix:
e.g. For Telegram: telegram_bot_token
telegram_chat_id
disable_web_preview
html_support
incl_poster
incl_subject
Notify actions with 'trigger_' prefix (trigger_on_play, trigger_on_stop, etc.),
and notify text with 'text_' prefix (text_on_play_subject, text_on_play_body, etc.) are optional.
telegram_disable_web_preview
telegram_html_support
telegram_incl_poster
telegram_incl_subject
Notify actions (int): 0 or 1,
e.g. on_play, on_stop, etc.
Notify text (str):
e.g. on_play_subject, on_play_body, etc.
Returns:
None
@@ -3126,8 +3129,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def get_browser_notifications(self, **kwargs):
browser = notifiers.BROWSER()
result = browser.get_notifications()
result = notifiers.get_browser_notifications()
if result:
notifications = result['notifications']
@@ -3198,6 +3200,16 @@ class WebInterface(object):
logger.warn(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
@requireAuth(member_of("admin"))
def set_notification_config(self, **kwargs):
@@ -3546,13 +3558,20 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_changelog(self, latest_only=False, update_shown=False, **kwargs):
latest_only = True if latest_only == 'true' else False
def get_changelog(self, latest_only=False, since_prev_release=False, update_shown=False, **kwargs):
latest_only = (latest_only == 'true')
since_prev_release = (since_prev_release == 'true')
if since_prev_release and plexpy.PREV_RELEASE == common.VERSION_NUMBER:
latest_only = True
since_prev_release = False
# Set update changelog shown status
if update_shown == 'true':
plexpy.CONFIG.__setattr__('UPDATE_SHOW_CHANGELOG', 0)
plexpy.CONFIG.write()
return versioncheck.read_changelog(latest_only=latest_only)
return versioncheck.read_changelog(latest_only=latest_only, since_prev_release=since_prev_release)
##### Info #####
@@ -3572,6 +3591,8 @@ class WebInterface(object):
if metadata:
poster_info = data_factory.get_poster_info(metadata=metadata)
metadata.update(poster_info)
lookup_info = data_factory.get_lookup_info(metadata=metadata)
metadata.update(lookup_info)
else:
pms_connect = pmsconnect.PmsConnect()
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
@@ -3579,6 +3600,8 @@ class WebInterface(object):
data_factory = datafactory.DataFactory()
poster_info = data_factory.get_poster_info(metadata=metadata)
metadata.update(poster_info)
lookup_info = data_factory.get_lookup_info(metadata=metadata)
metadata.update(lookup_info)
if metadata:
if metadata['section_id'] and not allow_session_library(metadata['section_id']):
@@ -3863,15 +3886,60 @@ class WebInterface(object):
@cherrypy.expose
@cherrypy.tools.json_out()
@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()
result = data_factory.delete_poster_url(rating_key=rating_key)
if result:
return {'result': 'success', 'message': 'Deleted Imgur poster url.'}
return {'result': 'success', 'message': 'Deleted Imgur poster.'}
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 #####
@@ -4097,11 +4165,15 @@ class WebInterface(object):
],
"added_at": "1461572396",
"art": "/library/metadata/1219/art/1462175063",
"audience_rating": "8",
"banner": "/library/metadata/1219/banner/1462175063",
"collections": [],
"content_rating": "TV-MA",
"directors": [
"Jeremy Podeswa"
],
"duration": "2998290",
"full_title": "Game of Thrones - The Red Woman",
"genres": [
"Adventure",
"Drama",
@@ -4115,6 +4187,74 @@ class WebInterface(object):
"last_viewed_at": "1462165717",
"library_name": "TV Shows",
"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",
"originally_available_at": "2016-04-24",
"parent_media_index": "6",
@@ -4124,11 +4264,13 @@ class WebInterface(object):
"rating": "7.8",
"rating_key": "153037",
"section_id": "2",
"sort_title": "Game of Thrones",
"studio": "HBO",
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
"tagline": "",
"thumb": "/library/metadata/153037/thumb/1462175060",
"title": "The Red Woman",
"user_rating": "9.0",
"updated_at": "1462175060",
"writers": [
"David Benioff",
@@ -4367,67 +4509,219 @@ class WebInterface(object):
Returns:
json:
{"stream_count": 3,
"sessions":
[{"art": "/library/metadata/1219/art/1462175063",
"aspect_ratio": "1.78",
"audio_channels": "6",
"audio_codec": "ac3",
"audio_decision": "transcode",
"bif_thumb": "/library/parts/274169/indexes/sd/",
"bitrate": "10617",
"container": "mkv",
"content_rating": "TV-MA",
"duration": "2998290",
"friendly_name": "Mother of Dragons",
"grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones",
"height": "1078",
"indexes": 1,
"ip_address": "xxx.xxx.xxx.xxx",
"labels": [],
"machine_id": "83f189w617623ccs6a1lqpby",
"media_index": "1",
"media_type": "episode",
"parent_media_index": "6",
"parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
"parent_title": "",
"platform": "Chrome",
"player": "Plex Web (Chrome)",
"progress_percent": "0",
"rating_key": "153037",
"section_id": "2",
"session_key": "291",
"state": "playing",
"throttled": "1",
"thumb": "/library/metadata/153037/thumb/1462175060",
"title": "The Red Woman",
"transcode_audio_channels": "2",
"transcode_audio_codec": "aac",
"transcode_container": "mkv",
"transcode_height": "1078",
"transcode_key": "tiv5p524wcupe8nxegc26s9k9",
"transcode_progress": 2,
"transcode_protocol": "http",
"transcode_speed": "0.0",
"transcode_video_codec": "h264",
"transcode_width": "1920",
"user": "DanyKhaleesi69",
"user_id": 8008135,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar",
"video_codec": "h264",
"video_decision": "copy",
"video_framerate": "24p",
"video_resolution": "1080",
"view_offset": "",
"width": "1920",
"year": "2016"
},
{...},
{...}
]
{"lan_bandwidth": 25318,
"sessions": [
{
"actors": [
"Kit Harington",
"Emilia Clarke",
"Isaac Hempstead-Wright",
"Maisie Williams",
"Liam Cunningham",
],
"added_at": "1461572396",
"allow_guest": 1,
"art": "/library/metadata/1219/art/1503306930",
"aspect_ratio": "1.78",
"audience_rating": "",
"audio_bitrate": "384",
"audio_bitrate_mode": "",
"audio_channel_layout": "5.1(side)",
"audio_channels": "6",
"audio_codec": "ac3",
"audio_decision": "direct play",
"audio_language": "",
"audio_language_code": "",
"audio_profile": "",
"audio_sample_rate": "48000",
"bandwidth": "25318",
"banner": "/library/metadata/1219/banner/1503306930",
"bif_thumb": "/library/parts/274169/indexes/sd/1000",
"bitrate": "10617",
"channel_stream": 0,
"collections": [],
"container": "mkv",
"content_rating": "TV-MA",
"deleted_user": 0,
"device": "Windows",
"directors": [
"Jeremy Podeswa"
],
"do_notify": 0,
"duration": "2998272",
"email": "Jon.Snow.1337@CastleBlack.com",
"file": "/media/TV Shows/Game of Thrones/Season 06/Game of Thrones - S06E01 - The Red Woman.mkv",
"file_size": "3979115377",
"friendly_name": "Jon Snow",
"full_title": "Game of Thrones - The Red Woman",
"genres": [
"Adventure",
"Drama",
"Fantasy"
],
"grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1503306930",
"grandparent_title": "Game of Thrones",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"height": "1078",
"id": "",
"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
}
```
"""
@@ -4457,7 +4751,7 @@ class WebInterface(object):
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
if s['location'] == 'lan':
counts['lan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
elif s['location'] == 'wan':
else:
counts['wan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
result.update(counts)
@@ -4564,27 +4858,29 @@ class WebInterface(object):
Returns:
json:
[{"content_type": "video",
[{"audio_bitrate": "192",
"client_id": "95434se643fsf24f-com-plexapp-android",
"content_type": "video",
"device_name": "Tyrion's iPad",
"failure": "",
"friendly_name": "Tyrion Lannister",
"item_complete_count": "0",
"item_complete_count": "1",
"item_count": "1",
"item_downloaded_count": "0",
"item_downloaded_percent_complete": 0,
"item_downloaded_count": "1",
"item_downloaded_percent_complete": 100,
"metadata_type": "movie",
"music_bitrate": "192",
"photo_quality": "74",
"platform": "iOS",
"rating_key": "154092",
"root_title": "Deadpool",
"state": "pending",
"root_title": "Movies",
"state": "complete",
"sync_id": "11617019",
"title": "Deadpool",
"total_size": "0",
"sync_title": "Deadpool",
"total_size": "560718134",
"user": "DrukenDwarfMan",
"user_id": "696969",
"username": "DrukenDwarfMan",
"video_quality": "60"
"video_bitrate": "4000"
"video_quality": "100"
},
{...},
{...}