Compare commits

...

82 Commits

Author SHA1 Message Date
JonnyWong16
70b6bd4efd v2.1.13 2018-06-16 16:42:20 -07:00
JonnyWong16
4f4a99d695 Fix soft crash when viewing photos not in an album 2018-06-16 16:38:06 -07:00
JonnyWong16
f65baa22f1 Fix font awesome icon in setting alerts 2018-06-12 19:28:33 -07:00
JonnyWong16
a1fb081d47 Fix font awesome info page breadcrumb icon 2018-06-11 11:36:50 -07:00
JonnyWong16
63ac3eec98 Fix font awesome remove ajax loader icon 2018-06-11 11:30:51 -07:00
JonnyWong16
73c11c053d Add clip to notification param media type example 2018-06-11 11:11:51 -07:00
JonnyWong16
85c398ce05 Change graph resolution font awesome icon 2018-06-10 19:55:31 -07:00
JonnyWong16
7387cb8322 Add date and time parameters for notifications and newsletters 2018-06-10 19:48:46 -07:00
JonnyWong16
37841b6e8c Fix font awesome icons in notifier text accordions 2018-06-10 19:36:32 -07:00
JonnyWong16
af1355e220 Fix blank font awesome icons 2018-06-10 18:23:22 -07:00
JonnyWong16
37bd005907 Update Discord links 2018-06-10 10:48:05 -07:00
JonnyWong16
311030b58e Add buttons to Discord/Reddit on support page 2018-06-10 10:47:48 -07:00
JonnyWong16
bdd124327f Update to Font Awesome v5 2018-06-10 10:15:49 -07:00
JonnyWong16
70e5e698fa Add support page 2018-06-09 21:28:40 -07:00
JonnyWong16
33e799955e Correct sessions.product column type 2018-06-09 00:03:23 -07:00
JonnyWong16
331be52327 v2.1.12 2018-06-08 22:12:51 -07:00
JonnyWong16
6ece690e23 Fix grouping plays on graphs with new grouping logic 2018-06-08 18:20:51 -07:00
JonnyWong16
6b63a1399e Add collection tag to raw newsletter data 2018-06-08 17:42:20 -07:00
JonnyWong16
5c77cf652b Fix typo in Join notifier help text 2018-06-08 17:18:05 -07:00
JonnyWong16
d6f9a82edb Fix mutable default arguments 2018-06-07 18:51:52 -07:00
JonnyWong16
a887489666 Change notifier link sources to blank for disabled 2018-06-07 18:51:20 -07:00
JonnyWong16
8c0bcd0059 Ability to stop a stream using session key 2018-06-05 20:30:07 -07:00
JonnyWong16
c18ee81130 Fix typo in http handler 2018-06-03 12:25:15 -07:00
JonnyWong16
44428cc6e5 Update logger blacklist for newsleter/notifier configs 2018-06-03 11:01:58 -07:00
JonnyWong16
c19cc858bd v2.1.11-beta 2018-06-02 10:39:32 -07:00
JonnyWong16
668913fd60 Allow manual PMS URL override in config file for XML shortcuts 2018-06-02 09:55:39 -07:00
JonnyWong16
50c5407a46 Fix Monitor Remote Access checkbox (Plex server update chaged bool to int) 2018-06-02 09:32:05 -07:00
JonnyWong16
939755d3b7 Add Plex XML shortcuts to libraries, users, and sync headers 2018-06-02 08:58:55 -07:00
JonnyWong16
54f4696713 Refactor open Plex XML script 2018-06-02 08:53:53 -07:00
JonnyWong16
c85af521fe Add Plex XML shortcuts for metadata and server resources 2018-06-02 08:26:30 -07:00
JonnyWong16
917d19db85 Fix grouping or new plays not previously in history 2018-06-01 18:45:58 -07:00
JonnyWong16
7292f25eb9 Reword group play history setting 2018-05-31 09:10:44 -07:00
JonnyWong16
22a2ad4bc7 Fix progress percent in grouping logic 2018-05-31 08:34:58 -07:00
JonnyWong16
95e56f5ea5 Fix activity progress bar not updating in some cases 2018-05-31 08:33:50 -07:00
JonnyWong16
ed24232a0a Improve logic for grouping history items 2018-05-30 22:18:41 -07:00
JonnyWong16
15225faee7 Add filename to notification parameters 2018-05-30 21:52:18 -07:00
JonnyWong16
041a35a35a Fallback to title if missing index for update metadata 2018-05-30 21:46:31 -07:00
JonnyWong16
6d365c174a Update X-Plex headers 2018-05-30 21:39:45 -07:00
JonnyWong16
b5f2f55972 v2.1.10-beta 2018-05-28 17:22:16 -07:00
JonnyWong16
ac207260c8 Do not send newsletter if failed to render template 2018-05-27 23:02:56 -07:00
JonnyWong16
e93808381c Fix track listing layout on info pages 2018-05-27 22:43:56 -07:00
JonnyWong16
7acb8f7dc5 Fix artist summary not showing up on newsletter 2018-05-27 22:35:54 -07:00
JonnyWong16
ba9f4a1f9e Use track artist for music 2018-05-27 22:24:43 -07:00
JonnyWong16
8502c28e25 Fallback poster_key and poster_title for clip notification 2018-05-27 15:39:28 -07:00
JonnyWong16
10add90451 Merge pull request #1295 from samwiseg00/feature-add-timestamp-discord
Add timestamps for rich metadata info on discord
2018-05-27 14:47:53 -07:00
samwiseg00
ddb7fa04ca add timestamps for rich metadata info on discord 2018-05-27 17:44:35 -04:00
JonnyWong16
e21a13b7ff Revert "Hack to check for live tv stopped websocket event"
This reverts commit 1245b4fbd3.
2018-05-27 14:13:24 -07:00
JonnyWong16
1245b4fbd3 Hack to check for live tv stopped websocket event 2018-05-27 14:04:47 -07:00
JonnyWong16
94b00c75c2 Enable notifications for clip media type 2018-05-27 13:41:56 -07:00
JonnyWong16
2edcf26110 Use HTTPS for cloudinary urls 2018-05-27 13:07:18 -07:00
JonnyWong16
a9fdf73e8b Check live tv websocket event using key instead of rating key 2018-05-27 13:00:34 -07:00
JonnyWong16
4884cee309 Fix live tv stream resolution 2018-05-27 10:13:42 -07:00
JonnyWong16
b3c7256bcf Newsletter footer inherit styles 2018-05-26 17:29:21 -07:00
JonnyWong16
2c9a7ced13 Forgot product in session db write 2018-05-26 10:14:54 -07:00
JonnyWong16
aa365eb6a3 Improved checking of live tv session websocket events 2018-05-26 10:14:36 -07:00
JonnyWong16
2366a8811b Catch exception from failed SMTP connection 2018-05-25 12:19:46 -07:00
JonnyWong16
53aafbd19e Fix typo from d5bffc3 2018-05-25 12:18:29 -07:00
JonnyWong16
d5bffc374c Fallback to blank poster/art on newsletter if image hosting is disabled 2018-05-25 08:26:25 -07:00
JonnyWong16
5cd5c36d8c Actually add live notification parameter 2018-05-23 17:17:47 -07:00
JonnyWong16
7f9e8f6211 Clean up script.js 2018-05-23 17:13:20 -07:00
JonnyWong16
f743a817ba Update python-twitter to 3.4.1 2018-05-23 17:12:19 -07:00
JonnyWong16
8e4aba7ed4 v2.1.9 2018-05-21 09:07:12 -07:00
JonnyWong16
8c0ef75d4c Fix typos and some cleanup 2018-05-21 09:07:01 -07:00
JonnyWong16
76c4b3bb71 Add Live to notification parameter 2018-05-21 08:49:35 -07:00
JonnyWong16
112b1c7984 Refactor css pointer class 2018-05-20 17:04:55 -07:00
JonnyWong16
c22a2513e3 Update CONTRIBUTING.md 2018-05-19 09:12:13 -07:00
JonnyWong16
f336782fc1 v2.1.8-beta 2018-05-19 09:07:18 -07:00
JonnyWong16
c19afa06de Fallback to originally available at for episode number on info pages 2018-05-18 17:47:19 -07:00
JonnyWong16
e003850d31 Update Facebook permissions scope 2018-05-18 17:41:42 -07:00
JonnyWong16
23cf790079 Return proper status codes for API (Fixes Tautulli/Tautulli-Issues#82) 2018-05-18 17:41:23 -07:00
JonnyWong16
e7f930bd0f Check for Tautulli footer in newsletters 2018-05-17 10:31:55 -07:00
JonnyWong16
348707b6b9 Revert back to HTTP newsletter images from tautulli.com 2018-05-17 09:30:34 -07:00
JonnyWong16
7ad78b4536 Allow images through newsletter password auth 2018-05-17 08:40:58 -07:00
JonnyWong16
a408a62234 Check newsletter auth setting when checking guest access enabled 2018-05-17 08:34:36 -07:00
JonnyWong16
a1e9e7e87f Add newsletter password to newsletter parameters 2018-05-16 23:20:53 -07:00
JonnyWong16
fa99f6e684 Add self-hosted newsletter authentication metnods 2018-05-16 23:11:28 -07:00
JonnyWong16
11e9bd2d54 Fix incorrect <div> tag 2018-05-16 21:59:15 -07:00
JonnyWong16
50165af4b7 Update tautulli.com URLs to HTTPS 2018-05-15 20:38:25 -07:00
JonnyWong16
5dd22c23f2 Patch Twitter str encoding for Python 2 2018-05-15 08:44:13 -07:00
JonnyWong16
79b45c1c46 Auto quality when fetching cloudinary transform 2018-05-15 08:43:20 -07:00
JonnyWong16
af917c4915 Add session key to activity processor log messages 2018-05-14 09:03:18 -07:00
JonnyWong16
c3238b5a83 Fix Imgur database migration again 2018-05-14 09:02:32 -07:00
65 changed files with 2622 additions and 1432 deletions

19
API.md
View File

@@ -434,6 +434,7 @@ Returns:
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"original_title": "",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210", "parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -678,6 +679,7 @@ Returns:
"full_title": "Game of Thrones - The Red Woman", "full_title": "Game of Thrones - The Red Woman",
"grandparent_rating_key": 351, "grandparent_rating_key": 351,
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
"original_title": "",
"group_count": 1, "group_count": 1,
"group_ids": "1124", "group_ids": "1124",
"id": 1124, "id": 1124,
@@ -1172,6 +1174,7 @@ Returns:
} }
], ],
"media_type": "episode", "media_type": "episode",
"original_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
@@ -1779,6 +1782,7 @@ Returns:
"library_name": "", "library_name": "",
"media_index": "1", "media_index": "1",
"media_type": "episode", "media_type": "episode",
"original_title": "",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -1953,6 +1957,7 @@ Returns:
"optimized_version": "", "optimized_version": "",
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"original_title": "",
"pre_tautulli": "", "pre_tautulli": "",
"quality_profile": "1.5 Mbps 480p", "quality_profile": "1.5 Mbps 480p",
"stream_audio_bitrate": 203, "stream_audio_bitrate": 203,
@@ -2545,7 +2550,7 @@ Returns:
### set_mobile_device_config ### set_mobile_device_config
Configure an exisitng notificaiton agent. Configure an existing notification agent.
``` ```
Required parameters: Required parameters:
@@ -2560,7 +2565,7 @@ Returns:
### set_newsletter_config ### set_newsletter_config
Configure an exisitng newsletter agent. Configure an existing newsletter agent.
``` ```
Required parameters: Required parameters:
@@ -2576,7 +2581,7 @@ Returns:
### set_notifier_config ### set_notifier_config
Configure an exisitng notificaiton agent. Configure an existing notification agent.
``` ```
Required parameters: Required parameters:
@@ -2619,15 +2624,15 @@ Returns:
### terminate_session ### terminate_session
Add a new notification agent. Stop a streaming session.
``` ```
Required parameters: Required parameters:
session_id (str): The id of the session to terminate session_key (int): The session key of the session to terminate, OR
message (str): A custom message to send to the client session_id (str): The session id of the session to terminate
Optional parameters: Optional parameters:
None message (str): A custom message to send to the client
Returns: Returns:
None None

View File

@@ -1,5 +1,70 @@
# Changelog # Changelog
## v2.1.13 (2018-06-16)
* Monitoring:
* Fix: Soft crash when viewing photos not in an album.
* Notifications:
* New: Added current date and time notification parameters.
* UI:
* New: Added support page with embedded Discord chat using WidgetBot.
## v2.1.12 (2018-06-08)
* Notifications:
* Change: Blank notification link source means disabled instead of default.
* Newsletters:
* New: Make collection tags available in the raw newsletter data for custom templates.
* API:
* New: Ability to terminate a stream using the session key.
## v2.1.11-beta (2018-06-02)
* Monitoring:
* Fix: Activity progress bar not updating in some cases.
* Fix: Monitory Remote Access setting disabled due to Plex Media Server API changes.
* Change: Improved logic for grouping history items without being successive plays.
* Notifications:
* New: Added filename to notification parameters.
* Other:
* Fix: Update metadata failing for tracks without track numbers.
## v2.1.10-beta (2018-05-28)
* Monitoring:
* Fix: Improved monitoring of live tv sessions.
* Change: Use track artist instead of album artist.
* Notifications:
* New: Added timestamp to Discord notification embeds. (Thanks @samwiseg00)
* New: Enable notifications for "clip" media types.
* Fix: Actually add the "live" notification parameter.
* Change: Update Twitter for 280 characters.
* Change: Use HTTPS url for Cloudinary images.
* Newsletters:
* Fix: Artist summaries not showing up on newsletter cards.
* Change: Do not send the newsletter if the template fails to render.
## v2.1.9 (2018-05-21)
* Notifications:
* New: Added "live" to notification parameters.
## v2.1.8-beta (2018-05-19)
* Newsletters:
* New: Added authentication options for self-hosted newsletters.
* Change: Check if the Tautulli footer has been removed in custom newsletter templates.
* Notifications:
* Fix: Cloudinary images not working for Twitter notifications.
* API:
* Fix: Return proper HTTP status codes for errors.
## v2.1.7-beta (2018-05-13) ## v2.1.7-beta (2018-05-13)
* Newsletters: * Newsletters:

View File

@@ -4,12 +4,12 @@
If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request. If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request.
### Branches ### Branches
All pull requests should be based on the `dev` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/dev -b FEATURE_NAME`. Use meaningful commit messages. All pull requests should be based on the `nightly` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/nightly -b FEATURE_NAME`. Use meaningful commit messages.
### Python Code ### Python Code
#### Compatibility #### Compatibility
The code should work with Python 2.7. Note that Tautulli 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 many different platforms.
Re-use existing code. Do not hesitate to add logging in your code. You can the logger module `plexpy.logger.*` for this. Web requests are invoked via `plexpy.request.*` and derived ones. Use these methods to automatically add proper and meaningful error handling. 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.
@@ -29,13 +29,10 @@ Although Tautulli did not adapt a code convention in the past, we try to follow
#### Documentation #### Documentation
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information. Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
#### Continuous Integration
Tautulli has a configuration file for [travis-ci](https://travis-ci.org/). You can add your forked repo to Travis to have it check your code against PEP8, PyLint, and PyFlakes for you. Your pull request will show a green check mark or a red cross on each tested commit, depending on if linting passes.
### HTML/Template code ### HTML/Template code
#### Compatibility #### Compatibility
HTML5 compatible browsers are targetted. There is no specific mobile version of Tautulli yet. HTML5 compatible browsers are targeted.
#### Conventions #### Conventions
* 4 space indentation * 4 space indentation

View File

@@ -1,6 +1,6 @@
# Tautulli # Tautulli
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/tQcWEUp) [![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://tautulli.com/discord)
[![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/) [![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/)
[![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server) [![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)
@@ -27,15 +27,15 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
## Preview ## Preview
* [Full preview gallery available on our website](http://tautulli.com) * [Full preview gallery available on our website](https://tautulli.com)
![Tautulli Homepage](http://tautulli.com/images/screenshots/activity-compressed.jpg?v=2) ![Tautulli Homepage](https://tautulli.com/images/screenshots/activity-compressed.jpg?v=2)
## Installation and Support ## Installation and Support
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli. * 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. * 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 is available on [Discord](https://tautulli.com/discord), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server).
## Issues & Feature Requests ## Issues & Feature Requests

View File

@@ -17,7 +17,6 @@
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" /> <link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet"> <link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet"> <link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
${next.headIncludes()} ${next.headIncludes()}
<!-- Favicons --> <!-- Favicons -->
@@ -134,7 +133,7 @@
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li> <li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
<li><a href="${anon_url('https://github.com/%s/%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="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
<li><a href="settings?support=true"><i class="fa fa-fw fa-comment"></i> Support</a></li> <li><a href="support"><i class="fa fa-fw fa-comment"></i> Support</a></li>
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li> <li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
@@ -289,9 +288,13 @@ ${next.modalIncludes()}
<script src="${http_root}js/jquery-2.1.4.min.js"></script> <script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/bootstrap.min.js"></script> <script src="${http_root}js/bootstrap.min.js"></script>
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script> <script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
<script>window.FontAwesomeConfig = {searchPseudoElements: true}</script>
<script src="${http_root}js/fontawesome-v5.0.13.min.js"></script>
<script src="${http_root}js/fontawesome-v4-shims.min.js"></script>
<script src="${http_root}js/pnotify.custom.min.js"></script> <script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script> <script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script> <script src="${http_root}js/jquery.qrcode.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS: % if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script> <script src="${http_root}js/ajaxNotifications.js"></script>
% endif % endif

View File

@@ -78,7 +78,7 @@ DOCUMENTATION :: END
<tr> <tr>
<td class="top-line">Resources:</td> <td class="top-line">Resources:</td>
<td class="top-line"> <td class="top-line">
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> | <a class="no-highlight" href="${anon_url('https://tautulli.com')}" target="_blank">Tautulli Website</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" 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 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" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Wiki</a> |
@@ -88,7 +88,7 @@ DOCUMENTATION :: END
<tr> <tr>
<td>Support:</td> <td>Support:</td>
<td> <td>
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/tQcWEUp')}" target="_blank">Tautulli Discord Server</a> | <a class="no-highlight support-modal-link" href="${anon_url('https://tautulli.com/discord')}" target="_blank">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> | <a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server')}" target="_blank">Plex Forums</a> <a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server')}" target="_blank">Plex Forums</a>
</td> </td>

View File

@@ -113,6 +113,10 @@ div.form-control .selectize-input {
.wizard-input-section .selectize-dropdown.form-control.selectize-pms-ip { .wizard-input-section .selectize-dropdown.form-control.selectize-pms-ip {
margin-top: 0 !important; margin-top: 0 !important;
} }
#condition-widget .fa-plus,
#condition-widget .fa-minus {
cursor: pointer;
}
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder { .react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
color: #fff !important; color: #fff !important;
} }
@@ -1599,10 +1603,8 @@ a:hover .dashboard-recent-media-cover {
margin: 0; margin: 0;
background: none; background: none;
} }
.summary-navbar-list .breadcrumb > li + li:before { .summary-navbar-list .breadcrumb > .breadcrumb-arrow {
color: #444; color: #444;
font-family: FontAwesome;
content: "\f054";
padding: 0 15px; padding: 0 15px;
} }
.summary-navbar-list .breadcrumb > .active { .summary-navbar-list .breadcrumb > .active {
@@ -2113,21 +2115,18 @@ a:hover .item-children-poster {
} }
.settings-alert ul li { .settings-alert ul li {
list-style: none; list-style: none;
padding: 5px 12px 5px 35px; padding: 5px 12px;
margin: 0; margin: 0;
border: 1px solid #ebccd1; border: 1px solid #ebccd1;
border-radius: 4px; border-radius: 4px;
} }
.settings-alert ul li:before { .settings-alert ul li:before {
content: "\f071"; display: none;
font-family: FontAwesome; content: "\f071 ";
font-style: normal; font-family: "Font Awesome 5 Solid";
font-weight: normal; }
text-decoration: inherit; .settings-alert ul li .svg-inline--fa {
font-size: 18px; margin-right: 5px;
position: absolute;
top: 5px;
left: 12px;
} }
.settings-warning { .settings-warning {
color: #eb8600; color: #eb8600;
@@ -2935,6 +2934,7 @@ a .home-platforms-list-cover-face:hover
} }
.stacked-configs > li > span > a.toggle-left, .stacked-configs > li > span > a.toggle-left,
.stacked-configs > li > span > span.toggle-left { .stacked-configs > li > span > span.toggle-left {
float: left;
color: #444; color: #444;
padding-right: 8px; padding-right: 8px;
} }
@@ -2945,16 +2945,6 @@ a .home-platforms-list-cover-face:hover
.stacked-configs > li > span > span.active { .stacked-configs > li > span > span.active {
color: #f9be03; color: #f9be03;
} }
.stacked-configs > li.new-notification-agent,
.stacked-configs > li.notification-agent,
.stacked-configs > li.add-notification-agent,
.stacked-configs > li.new-newsletter-agent,
.stacked-configs > li.newsletter-agent,
.stacked-configs > li.add-newsletter-agent,
.stacked-configs > li.mobile-device,
.stacked-configs > li.add-mobile-device {
cursor: pointer;
}
.stacked-configs > li.mobile-device > span > a.toggle-left, .stacked-configs > li.mobile-device > span > a.toggle-left,
.stacked-configs > li.mobile-device > span > span.toggle-left { .stacked-configs > li.mobile-device > span > span.toggle-left {
color: #999; color: #999;
@@ -2966,10 +2956,13 @@ a .home-platforms-list-cover-face:hover
background: #282828; background: #282828;
list-style: none; list-style: none;
} }
.accordion .link { .accordion li {
margin: 0;
}
.accordion li .link {
cursor: pointer; cursor: pointer;
display: block; display: block;
padding: 8px 20px 8px 30px; padding: 8px 12px 8px 12px;
color: #999; color: #999;
border-bottom: 1px solid #2d2d2d; border-bottom: 1px solid #2d2d2d;
position: relative; position: relative;
@@ -2977,37 +2970,28 @@ a .home-platforms-list-cover-face:hover
-o-transition: all 0.3s ease; -o-transition: all 0.3s ease;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.accordion li { .accordion li .link:hover {
margin: 0; color: #fff;
background: #2f2f2f;
}
.accordion li .link span.toggle-right {
float: right;
padding-left: 10px;
} }
.accordion li:last-child .link { .accordion li:last-child .link {
border-bottom: 0; border-bottom: 0;
} }
.accordion li i.fa { .accordion li .fa-chevron-down {
position: absolute;
top: 10px;
left: 10px;
color: #999; color: #999;
-webkit-transition: all 0.3s ease; -webkit-transition: all 0.3s ease;
-o-transition: all 0.3s ease; -o-transition: all 0.3s ease;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.accordion li i.fa-chevron-down {
right: 12px;
left: auto;
font-size: 16px;
}
.accordion li .link:hover {
color: #FFF;
background: #2f2f2f;
}
.accordion li.open .link { .accordion li.open .link {
color: #f9be03; color: #f9be03;
} }
.accordion li.open i { .accordion li.open .fa-chevron-down {
color: #f9be03; color: #f9be03;
}
.accordion li.open i.fa-chevron-down {
-webkit-transform: rotate(180deg); -webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg); -ms-transform: rotate(180deg);
-o-transform: rotate(180deg); -o-transform: rotate(180deg);
@@ -3165,34 +3149,6 @@ div.dataTables_info {
-o-transition: none !important; -o-transition: none !important;
transition: none !important; transition: none !important;
} }
.settings-alert {
float: left;
padding: 0;
margin: 5px 0;
border: 0;
position: relative;
}
.settings-alert ul {
padding: 0;
}
.settings-alert ul li {
list-style: none;
padding: 5px 12px 5px 35px;
margin: 0;
border: 1px solid #ebccd1;
border-radius: 4px;
}
.settings-alert ul li:before {
content: "\f071";
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
text-decoration: inherit;
font-size: 18px;
position: absolute;
top: 5px;
left: 12px;
}
#users-to-delete > li, #users-to-delete > li,
#users-to-purge > li, #users-to-purge > li,
#libraries-to-delete > li, #libraries-to-delete > li,
@@ -3525,8 +3481,7 @@ a.no-highlight:hover {
} }
.login-logo { .login-logo {
margin: 0 auto 50px auto; margin: 0 auto 50px auto;
width: 340px; text-align: center;
height: 100px;
} }
.login-container .form-group { .login-container .form-group {
margin-bottom: 20px; margin-bottom: 20px;
@@ -4098,4 +4053,70 @@ a[data-tab-destination] {
margin-top: 10px !important; margin-top: 10px !important;
padding-top: 10px; padding-top: 10px;
border-top: 1px solid #444; border-top: 1px solid #444;
}
.newsletter-logo {
margin: 0 auto 50px auto;
text-align: center;
}
.pointer {
cursor: pointer;
}
.iframe-container {
width: 100%;
height: calc(100vh - 200px);
position: relative;
}
.iframe-overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: url(../images/discord-overlay.png) no-repeat;
background-size: cover;
border: 1px solid #36393e;
}
.iframe-button-container {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.iframe-button {
color: #fff;
border-radius: 20px;
text-align: center;
cursor: pointer;
font-size: 15px;
height: 28px;
line-height: 28px;
min-width: 200px;
transition: box-shadow 0.3s ease;
padding: 0 15px;
background: rgba(114, 137, 218, 0.4);
text-transform: uppercase;
text-decoration: none;
display: block;
}
.iframe-button:hover,
.iframe-button:focus {
color: #fff;
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 99999px inset, rgba(0, 0, 0, 0.2) 0px 1px 5px 0px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 3px 1px -2px;
}
.iframe-button:active {
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 99999px inset, rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, rgba(0, 0, 0, 0.12) 0px 3px 14px 2px;
}
.iframe {
width: 100%;
height: 100%;
position: inherit;
display: block;
border: 0;
}
.fa-blank {
visibility: hidden;
} }

View File

@@ -387,8 +387,8 @@ DOCUMENTATION :: END
<a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a> <a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
- <a href="${href}" title="${data['title']}">${data['title']}</a> - <a href="${href}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a> <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
- <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a> - <a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['original_title'] or data['grandparent_title']}">${data['original_title'] or data['grandparent_title']}</a>
% elif data['media_type'] == 'photo': % elif data['media_type'] == 'photo':
<span title="${data['parent_title']}">${data['parent_title']}</span> <span title="${data['parent_title']}">${data['parent_title']}</span>
% elif data['media_type'] == 'clip': % elif data['media_type'] == 'clip':

View File

@@ -168,7 +168,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4> <h4><i class="fa fa-expand-arrows-alt"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block"> <p class="help-block">
The combined total of tv and movies by their original resolution (pre-transcoding). The combined total of tv and movies by their original resolution (pre-transcoding).
</p> </p>
@@ -181,7 +181,7 @@
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4> <h4><i class="fa fa-expand-arrows-alt"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block"> <p class="help-block">
The combined total of tv and movies by their streamed resolution (post-transcoding). The combined total of tv and movies by their streamed resolution (post-transcoding).
</p> </p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -12,7 +12,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="padded-header" id="current-activity-header"> <div class="padded-header" id="current-activity-header">
<h3><span id="sessions-shortcut">Activity</span> &nbsp;&nbsp; <h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp;
<small> <small>
<span id="currentActivityHeader" style="display: none;"> <span id="currentActivityHeader" style="display: none;">
Streams: <span id="currentActivityHeader-streams"></span> | Streams: <span id="currentActivityHeader-streams"></span> |
@@ -236,7 +236,6 @@
<script src="${http_root}js/moment-with-locale.js"></script> <script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.scrollbar.min.js"></script> <script src="${http_root}js/jquery.scrollbar.min.js"></script>
<script src="${http_root}js/jquery.mousewheel.min.js"></script> <script src="${http_root}js/jquery.mousewheel.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
<script> <script>
var date_format = 'YYYY-MM-DD'; var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a'; var time_format = 'hh:mm a';
@@ -390,8 +389,8 @@
$('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true)'); $('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true)');
$('#metadata-grandparent_title-' + key) $('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key) .attr('href', 'info?rating_key=' + s.grandparent_rating_key)
.attr('title', s.grandparent_title) .attr('title', s.original_title || s.grandparent_title)
.text(s.grandparent_title); .text(s.original_title || s.grandparent_title);
} }
// Update cover if album changed // Update cover if album changed
if (s.parent_rating_key !== instance.data('parent_rating_key')) { if (s.parent_rating_key !== instance.data('parent_rating_key')) {
@@ -406,7 +405,11 @@
.text(s.parent_title); .text(s.parent_title);
} }
// Update cover if track changed // Update cover if track changed
if (s.parent_rating_key !== instance.data('parent_rating_key')) { if (s.rating_key !== instance.data('rating_key')) {
$('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
.attr('title', s.original_title || s.grandparent_title)
.text(s.original_title || s.grandparent_title);
$('#metadata-title-' + key) $('#metadata-title-' + key)
.attr('href', 'info?rating_key=' + s.rating_key) .attr('href', 'info?rating_key=' + s.rating_key)
.attr('title', s.title) .attr('title', s.title)
@@ -542,7 +545,7 @@
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%'); .attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
var progress_bar = $('#progress-bar-' + key); var progress_bar = $('#progress-bar-' + key);
progress_bar.data('state', s.state); progress_bar.data('state', s.state);
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) { if (progress_bar.data('last_view_offset') !== s.view_offset) {
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
} }
@@ -672,6 +675,7 @@
$.ajax({ $.ajax({
url: 'terminate_session', url: 'terminate_session',
data: { data: {
session_key: key,
session_id: session_id, session_id: session_id,
message: message message: message
}, },
@@ -690,10 +694,8 @@
}); });
}); });
$('#sessions-shortcut').on('tripleclick', function () { $('#sessions-xml').on('tripleclick', function () {
$.getJSON('return_sessions_url', function(sessions_url) { openPlexXML('/status/sessions');
window.open(sessions_url, '_blank');
});
}); });
% endif % endif
</script> </script>

View File

@@ -83,31 +83,44 @@ DOCUMENTATION :: END
<ul class="list-unstyled breadcrumb"> <ul class="list-unstyled breadcrumb">
% if data['media_type'] in ('movie', 'collection'): % if data['media_type'] in ('movie', 'collection'):
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'show': % elif data['media_type'] == 'show':
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'season': % elif data['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Season ${data['media_index']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Season ${data['media_index']}</li>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
<li class="active">Episode ${data['media_index']} - ${data['title']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
% elif data['media_type'] == 'artist': % elif data['media_type'] == 'artist':
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'album': % elif data['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">${data['title']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Track ${data['media_index']} - ${data['title']}</li> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
% endif % endif
</ul> </ul>
</div> </div>
@@ -165,7 +178,7 @@ DOCUMENTATION :: END
<h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1> <h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
<h2>${data['title']}</h2> <h2>${data['title']}</h2>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1> <h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['original_title'] or data['grandparent_title']}</a></h1>
<h2><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2> <h2><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2>
<h3 class="hidden-xs">T${data['media_index']}</h3> <h3 class="hidden-xs">T${data['media_index']}</h3>
% endif % endif
@@ -371,7 +384,11 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-header"> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
% if data['media_type'] in ('artist', 'album', 'track'):
<span>Play History for <strong>${data['title']}</strong></span>
% else:
<span>Watch History for <strong>${data['title']}</strong></span> <span>Watch History for <strong>${data['title']}</strong></span>
% endif
</div> </div>
<div class="button-bar"> <div class="button-bar">
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -502,7 +519,7 @@ DOCUMENTATION :: END
% elif data['media_type'] == 'album': % elif data['media_type'] == 'album':
${data['parent_title']}<br />${data['title']} ${data['parent_title']}<br />${data['title']}
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
${data['grandparent_title']}<br />${data['title']}<br />${data['parent_title']} ${data['original_title'] or data['grandparent_title']}<br />${data['title']}<br />${data['parent_title']}
% endif % endif
</strong> </strong>
</p> </p>
@@ -699,6 +716,10 @@ DOCUMENTATION :: END
</script> </script>
% endif % endif
<script> <script>
$('.metadata-xml').on('tripleclick', function () {
openPlexXML("/library/metadata/${data['rating_key']}");
});
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY')); $("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true)); $("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 }); $('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });

View File

@@ -91,7 +91,7 @@ DOCUMENTATION :: END
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"> <div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
<div class="item-children-card-overlay"> <div class="item-children-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
Episode ${child['media_index']} Episode ${child['media_index'] or child['originally_available_at']}
</div> </div>
</div> </div>
</div> </div>
@@ -122,16 +122,24 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'track': % elif data['children_type'] == 'track':
% if loop.index % 2 == 0: % if loop.index % 2 == 0:
<div class="item-children-list-item-even"> <div class="item-children-list-item-even">
<span class="item-children-list-item-index">${child['media_index']}</span> <span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span> <span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
% if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span>
% endif
</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}"> <span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script> <script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span> </span>
</div> </div>
% else: % else:
<div class="item-children-list-item-odd"> <div class="item-children-list-item-odd">
<span class="item-children-list-item-index">${child['media_index']}</span> <span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span> <span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
% if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span>
% endif
</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}"> <span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script> <script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span> </span>

View File

@@ -251,7 +251,7 @@ DOCUMENTATION :: END
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
<div class="item-children-instance-text-wrapper album-item"> <div class="item-children-instance-text-wrapper album-item">
<h3 title="${child['grandparent_title']}">${child['grandparent_title']}</h3> <h3 title="${child['original_title'] or child['grandparent_title']}">${child['original_title'] or child['grandparent_title']}</h3>
<h3 title="${child['title']}">${child['title']}</h3> <h3 title="${child['title']}">${child['title']}</h3>
<h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3> <h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3>
</div> </div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,6 @@
function initConfigCheckbox(elem, toggleElem = null, reverse = false) { function initConfigCheckbox(elem, toggleElem, reverse) {
toggleElem = (toggleElem === undefined) ? null : toggleElem;
reverse = (reverse === undefined) ? false : reverse;
var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next(); var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
config.css('overflow', 'hidden'); config.css('overflow', 'hidden');
if ($(elem).is(":checked")) { if ($(elem).is(":checked")) {
@@ -35,8 +37,8 @@ function showMsg(msg, loader, timeout, ms, error) {
} }
var message = $("<div class='msg'>" + msg + "</div>"); var message = $("<div class='msg'>" + msg + "</div>");
if (loader) { if (loader) {
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>"); message = $("<div class='msg'><i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
feedback.css("padding", "14px 10px") feedback.css("padding", "14px 10px");
} }
if (error) { if (error) {
feedback.css("background-color", "rgba(255,0,0,0.5)"); feedback.css("background-color", "rgba(255,0,0,0.5)");
@@ -59,7 +61,7 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
$('#confirm-modal').modal(); $('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () { $('#confirm-modal').one('click', '#confirm-button', function () {
if (loader_msg) { if (loader_msg) {
showMsg(loader_msg, true, false) showMsg(loader_msg, true, false);
} }
$.ajax({ $.ajax({
url: url, url: url,
@@ -71,9 +73,9 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = result.message; var msg = result.message;
if (result.result == 'success') { if (result.result == 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000) showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
} else { } else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true) showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true);
} }
if (typeof callback === "function") { if (typeof callback === "function") {
callback(result); callback(result);
@@ -85,8 +87,8 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
function doAjaxCall(url, elem, reload, form, showMsg, callback) { function doAjaxCall(url, elem, reload, form, showMsg, callback) {
// Set Message // Set Message
feedback = (showMsg) ? $("#ajaxMsg") : $(); var feedback = (showMsg) ? $("#ajaxMsg") : $();
update = $("#updatebar"); var update = $("#updatebar");
if (update.is(":visible")) { if (update.is(":visible")) {
var height = update.height() + 35; var height = update.height() + 35;
feedback.css("bottom", height + "px"); feedback.css("bottom", height + "px");
@@ -96,22 +98,23 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
feedback.fadeIn(); feedback.fadeIn();
// Get Form data // Get Form data
var formID = "#" + url; var formID = "#" + url;
if (form == true) { var dataString;
var dataString = $(formID).serialize(); if (form === true) {
dataString = $(formID).serialize();
} }
// Loader Image // Loader Image
var loader = $("<i class='fa fa-refresh fa-spin'></i>"); var loader = $("<i class='fa fa-refresh fa-spin ajaxLoader-" + url +"></i>");
// Data Success Message // Data Success Message
var dataSucces = $(elem).data('success'); var dataSucces = $(elem).data('success');
if (typeof dataSucces === "undefined") { if (typeof dataSucces === "undefined") {
// Standard Message when variable is not set // Standard Message when variable is not set
var dataSucces = "Success!"; dataSucces = "Success!";
} }
// Data Errror Message // Data Error Message
var dataError = $(elem).data('error'); var dataError = $(elem).data('error');
if (typeof dataError === "undefined") { if (typeof dataError === "undefined") {
// Standard Message when variable is not set // Standard Message when variable is not set
var dataError = "There was an error"; dataError = "There was an error";
} }
// Get Success & Error message from inline data, else use standard message // Get Success & Error message from inline data, else use standard message
var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>"); var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>");
@@ -120,7 +123,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
if (form) { if (form) {
if ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') || if ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ||
$('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) { $('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) {
feedback.addClass('error') feedback.addClass('error');
$(feedback).prepend(errorMsg); $(feedback).prepend(errorMsg);
setTimeout(function () { setTimeout(function () {
errorMsg.fadeOut(function () { errorMsg.fadeOut(function () {
@@ -128,7 +131,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
feedback.fadeOut(function () { feedback.fadeOut(function () {
feedback.removeClass('error'); feedback.removeClass('error');
}); });
}) });
$(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected'); $(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
}, 2000); }, 2000);
return false; return false;
@@ -144,33 +147,33 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
feedback.prepend(loader); feedback.prepend(loader);
}, },
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown) {
feedback.addClass('error') feedback.addClass('error');
feedback.prepend(errorMsg); feedback.prepend(errorMsg);
setTimeout(function () { setTimeout(function () {
errorMsg.fadeOut(function () { errorMsg.fadeOut(function () {
$(this).remove(); $(this).remove();
feedback.fadeOut(function () { feedback.fadeOut(function () {
feedback.removeClass('error') feedback.removeClass('error');
}); });
}) });
}, 2000); }, 2000);
}, },
success: function (data, jqXHR) { success: function (data, jqXHR) {
feedback.prepend(succesMsg); feedback.prepend(succesMsg);
feedback.addClass('success') feedback.addClass('success');
setTimeout(function (e) { setTimeout(function (e) {
succesMsg.fadeOut(function () { succesMsg.fadeOut(function () {
$(this).remove(); $(this).remove();
feedback.fadeOut(function () { feedback.fadeOut(function () {
feedback.removeClass('success'); feedback.removeClass('success');
}); });
if (reload == true) refreshSubmenu(); if (reload === true) refreshSubmenu();
if (reload == "table") { if (reload === "table") {
refreshTable(); refreshTable();
} }
if (reload == "tabs") refreshTab(); if (reload === "tabs") refreshTab();
if (reload == "page") location.reload(); if (reload === "page") location.reload();
if (reload == "submenu&table") { if (reload === "submenu&table") {
refreshSubmenu(); refreshSubmenu();
refreshTable(); refreshTable();
} }
@@ -179,12 +182,12 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
$(formID + " select").children('option[disabled=disabled]').attr( $(formID + " select").children('option[disabled=disabled]').attr(
'selected', 'selected'); 'selected', 'selected');
} }
}) });
}, 2000); }, 2000);
}, },
complete: function (jqXHR, textStatus) { complete: function (jqXHR, textStatus) {
// Remove loaders and stuff, ajax request is complete! // Remove loaders and stuff, ajax request is complete!
loader.remove(); feedback.remove('.ajaxLoader-' + url);
if (typeof callback === "function") { if (typeof callback === "function") {
callback(jqXHR); callback(jqXHR);
} }
@@ -215,19 +218,20 @@ function isPrivateIP(ip_address) {
$.cachedScript('js/ipaddr.min.js').done(function () { $.cachedScript('js/ipaddr.min.js').done(function () {
if (ipaddr.isValid(ip_address)) { if (ipaddr.isValid(ip_address)) {
var addr = ipaddr.process(ip_address) var addr = ipaddr.process(ip_address);
var rangeList = [];
if (addr.kind() === 'ipv4') { if (addr.kind() === 'ipv4') {
var rangeList = [ rangeList = [
ipaddr.parseCIDR('127.0.0.0/8'), ipaddr.parseCIDR('127.0.0.0/8'),
ipaddr.parseCIDR('10.0.0.0/8'), ipaddr.parseCIDR('10.0.0.0/8'),
ipaddr.parseCIDR('172.16.0.0/12'), ipaddr.parseCIDR('172.16.0.0/12'),
ipaddr.parseCIDR('192.168.0.0/16') ipaddr.parseCIDR('192.168.0.0/16')
] ];
} else { } else {
var rangeList = [ rangeList = [
ipaddr.parseCIDR('fd00::/8') ipaddr.parseCIDR('fd00::/8')
] ];
} }
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) { if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
@@ -238,12 +242,13 @@ function isPrivateIP(ip_address) {
} else { } else {
defer.resolve('n/a'); defer.resolve('n/a');
} }
}) });
return defer.promise(); return defer.promise();
} }
function humanTime(seconds) { function humanTime(seconds) {
var text;
if (seconds >= 86400) { if (seconds >= 86400) {
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' + '<h3>' + text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' + '<h3>' +
Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' + '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' + '<h3>' +
@@ -265,6 +270,7 @@ function humanTime(seconds) {
} }
function humanTimeClean(seconds) { function humanTimeClean(seconds) {
var text;
if (seconds >= 86400) { if (seconds >= 86400) {
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + Math.floor(moment.duration(( text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + Math.floor(moment.duration((
seconds % 86400), 'seconds').asHours()) + ' hrs ' + Math.floor(moment.duration( seconds % 86400), 'seconds').asHours()) + ' hrs ' + Math.floor(moment.duration(
@@ -341,7 +347,7 @@ function getCookie(cname) {
for (var i = 0; i < ca.length; i++) { for (var i = 0; i < ca.length; i++) {
var c = ca[i]; var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1); while (c.charAt(0) == ' ') c = c.substring(1);
if (c.indexOf(name) == 0) return c.substring(name.length, c.length); if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
} }
return ""; return "";
} }
@@ -354,24 +360,24 @@ var Accordion = function (el, multiple) {
links.on('click', { links.on('click', {
el: this.el, el: this.el,
multiple: this.multiple multiple: this.multiple
}, this.dropdown) }, this.dropdown);
} };
Accordion.prototype.dropdown = function (e) { Accordion.prototype.dropdown = function (e) {
var $el = e.data.el; var $el = e.data.el;
$this = $(this), $this = $(this);
$next = $this.next(); $next = $this.next();
$next.slideToggle(); $next.slideToggle();
$this.parent().toggleClass('open'); $this.parent().toggleClass('open');
if (!e.data.multiple) { if (!e.data.multiple) {
$el.find('.submenu').not($next).slideUp().parent().removeClass('open'); $el.find('.submenu').not($next).slideUp().parent().removeClass('open');
}; }
} };
function clearSearchButton(tableName, table) { function clearSearchButton(tableName, table) {
$('#' + tableName + '_filter').find('input[type=search]').wrap( $('#' + tableName + '_filter').find('input[type=search]').wrap(
'<div class="input-group" role="group" aria-label="Search"></div>').after( '<div class="input-group" role="group" aria-label="Search"></div>').after(
'<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' + '<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' +
tableName + '"><i class="fa fa-remove"></i></button></span>') tableName + '"><i class="fa fa-remove"></i></button></span>');
$('#clear-search-' + tableName).click(function () { $('#clear-search-' + tableName).click(function () {
table.search('').draw(); table.search('').draw();
}); });
@@ -401,7 +407,6 @@ $('*').on('click', '.refresh_pms_image', function (e) {
} else { } else {
if (pms_proxy_url.indexOf('refresh=true') > -1) { if (pms_proxy_url.indexOf('refresh=true') > -1) {
pms_proxy_url = pms_proxy_url.replace("&refresh=true", ""); pms_proxy_url = pms_proxy_url.replace("&refresh=true", "");
console.log(pms_proxy_url)
background_div.css('background-image', 'url(' + pms_proxy_url + ')'); background_div.css('background-image', 'url(' + pms_proxy_url + ')');
background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)'); background_div.css('background-image', 'url(' + pms_proxy_url + '&refresh=true)');
} else { } else {
@@ -416,8 +421,7 @@ function humanFileSize(bytes, si) {
if (Math.abs(bytes) < thresh) { if (Math.abs(bytes) < thresh) {
return bytes + ' B'; return bytes + ' B';
} }
var units = si var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
var u = -1; var u = -1;
do { do {
@@ -436,10 +440,10 @@ function forceMinMax(elem) {
if (isNaN(val)) { if (isNaN(val)) {
elem.val(default_val); elem.val(default_val);
} }
else if (min != undefined && val < min) { else if (min !== undefined && val < min) {
elem.val(min); elem.val(min);
} }
else if (max != undefined && val > max) { else if (max !== undefined && val > max) {
elem.val(max); elem.val(max);
} }
else { else {
@@ -453,4 +457,11 @@ function capitalizeFirstLetter(string) {
$.fn.slideToggleBool = function(bool, options) { $.fn.slideToggleBool = function(bool, options) {
return bool ? $(this).slideDown(options) : $(this).slideUp(options); return bool ? $(this).slideDown(options) : $(this).slideUp(options);
} };
function openPlexXML(endpoint, plextv, params) {
var data = $.extend({endpoint: endpoint, plextv: plextv}, params);
$.getJSON('return_plex_xml_url', data, function(xml_url) {
window.open(xml_url, '_blank');
});
}

View File

@@ -67,7 +67,7 @@ history_table_options = {
expand_history = '<span class="expand-history-tooltip" data-toggle="tooltip" title="Show Detailed History"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_history = '<span class="expand-history-tooltip" data-toggle="tooltip" title="Show Detailed History"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_history + '&nbsp;' + date + '</div></a></div>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_history + '&nbsp;' + date + '</div></a></div>');
} else { } else {
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i>&nbsp;' + date + '</div>'); $(td).html('<div style="float: left;"><i class="fa fa-plus-circle fa-fw fa-blank">&nbsp;</i>&nbsp;' + date + '</div>');
} }
}, },
"searchable": false, "searchable": false,

View File

@@ -58,7 +58,7 @@ media_info_table_options = {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else { } else {
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i>&nbsp;' + date + '</div>'); $(td).html('<div style="float: left;"><i class="fa fa-plus-circle fa-fw fa-blank"></i>&nbsp;' + date + '</div>');
} }
} }
}, },

View File

@@ -10,7 +10,7 @@
<div class='container-fluid'> <div class='container-fluid'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-book"></i> All Libraries</span> <span id="libraries-xml"><i class="fa fa-book"></i> All Libraries</span>
</div> </div>
<div class="button-bar"> <div class="button-bar">
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -198,5 +198,9 @@
}); });
}); });
% endif % endif
$('#libraries-xml').on('tripleclick', function () {
openPlexXML('/library/sections/all');
});
</script> </script>
</%def> </%def>

View File

@@ -11,12 +11,11 @@ DOCUMENTATION :: END
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for device in sorted(devices_list, key=lambda k: k['device_name']): % for device in sorted(devices_list, key=lambda k: k['device_name']):
<li class="mobile-device" data-id="${device['id']}" data-name="${device['device_name']}"> <li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
<span> <span>
<!--<span class="toggle-right mobile-device-tooltip edit-mobile-device" data-toggle="tooltip" data-placement="top" title="Edit Device"><i class="fa fa-lg fa-pencil"></i></span>--> <span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
<span class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span>
${device['friendly_name'] or device['device_name']} &nbsp;<span class="friendly_name">(${device['id']})</span> ${device['friendly_name'] or device['device_name']} &nbsp;<span class="friendly_name">(${device['id']})</span>
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
<span class="toggle-right friendly_name" id="device-last_seen-${device['id']}"> <span class="toggle-right friendly_name" id="device-last_seen-${device['id']}">
% if device['last_seen']: % if device['last_seen']:
<script> <script>
@@ -26,14 +25,13 @@ DOCUMENTATION :: END
never never
% endif % endif
</span> </span>
<!--<span class="toggle-right delete-mobile-device" data-toggle="tooltip" data-placement="top" title="Remove Device"><i class="fa fa-lg fa-times"></i></span>-->
</span> </span>
</li> </li>
% endfor % endfor
<li class="add-mobile-device" id="register-mobile-device" data-target="#api-qr-modal" data-toggle="modal"> <li class="add-mobile-device pointer" id="register-mobile-device" data-target="#api-qr-modal" data-toggle="modal">
<span> <span>
<span class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span> Register a new device <span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span> Register a new device
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
</span> </span>
</li> </li>
</ul> </ul>

View File

@@ -0,0 +1,43 @@
<%
import urllib
%>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tautulli - ${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="body-container">
<div class="container-fluid">
<div class="row">
<div class="login-container">
<div class="newsletter-logo">
<img src="${http_root}images/newsletter/newsletter-header.png" height="100" alt="PlexPy">
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<form action="${uri}" method="post" id="newsletter-form">
<div class="form-group">
<label for="password" class="control-label">
Password
</label>
<input type="password" id="key" name="key" class="form-control" autofocus>
</div>
<button id="enter" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Enter</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -12,15 +12,15 @@ DOCUMENTATION :: END
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %> <% from plexpy.newsletter_handler import NEWSLETTER_SCHED %>
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])): % for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
<li class="newsletter-agent" data-id="${newsletter['id']}"> <li class="newsletter-agent pointer" data-id="${newsletter['id']}">
<span> <span>
<span class="toggle-left trigger-tooltip ${'active' if newsletter['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Newsletter ${'active' if newsletter['active'] else 'inactive'}"><i class="fa fa-lg fa-newspaper-o"></i></span> <span class="toggle-left trigger-tooltip ${'active' if newsletter['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Newsletter ${'active' if newsletter['active'] else 'inactive'}"><i class="fa fa-lg fa-fw fa-newspaper-o"></i></span>
% if newsletter['friendly_name']: % if newsletter['friendly_name']:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span> ${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
% else: % else:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']})</span> ${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']})</span>
% endif % endif
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
<span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}"> <span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}">
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])): % if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %> <% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
@@ -32,10 +32,10 @@ DOCUMENTATION :: END
</span> </span>
</li> </li>
% endfor % endfor
<li class="add-newsletter-agent" id="add-newsletter-agent" data-target="#add-newsletter-modal" data-toggle="modal"> <li class="add-newsletter-agent pointer" id="add-newsletter-agent" data-target="#add-newsletter-modal" data-toggle="modal">
<span> <span>
<span class="toggle-left"><i class="fa fa-lg fa-newspaper-o"></i></span> Add a new newsletter agent <span class="toggle-left"><i class="fa fa-lg fa-fw fa-newspaper-o"></i></span> Add a new newsletter agent
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
</span> </span>
</li> </li>
</ul> </ul>

View File

@@ -1,7 +1,8 @@
% if notifier: % if notifier:
<%! <%!
import json import json
from plexpy import helpers, notifiers, users from plexpy import notifiers, users
from plexpy.helpers import checked
available_notification_actions = notifiers.available_notification_actions() available_notification_actions = notifiers.available_notification_actions()
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']] user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
@@ -70,7 +71,7 @@
% elif item['input_type'] == 'checkbox': % elif item['input_type'] == 'checkbox':
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${helpers.checked(item['value'])}> ${item['label']} <input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
</label> </label>
<p class="help-block">${item['description'] | n}</p> <p class="help-block">${item['description'] | n}</p>
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}"> <input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
@@ -146,7 +147,7 @@
% for action in available_notification_actions: % for action in available_notification_actions:
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${helpers.checked(notifier['actions'][action['name']])}> ${action['label']} <input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${checked(notifier['actions'][action['name']])}> ${action['label']}
</label> </label>
<p class="help-block">${action['description'] | n}</p> <p class="help-block">${action['description'] | n}</p>
<input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}"> <input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}">
@@ -201,7 +202,11 @@
% if notifier['agent_name'] == 'scripts': % if notifier['agent_name'] == 'scripts':
% for action in available_notification_actions: % for action in available_notification_actions:
<li> <li>
<div class="link"><i class="fa ${action['icon']} fa-fw"></i>&nbsp;${action['label']}<i class="fa fa-chevron-down"></i></div> <div class="link">
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>&nbsp;
${action['label']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@@ -223,7 +228,11 @@
% else: % else:
% for action in available_notification_actions: % for action in available_notification_actions:
<li> <li>
<div class="link"><i class="fa ${action['icon']} fa-fw"></i>&nbsp;${action['label']}<i class="fa fa-chevron-down"></i></div> <div class="link">
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>&nbsp;
${action['label']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">

View File

@@ -11,22 +11,22 @@ DOCUMENTATION :: END
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])): % for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])):
<li class="notification-agent" data-id="${notifier['id']}"> <li class="notification-agent pointer" data-id="${notifier['id']}">
<span> <span>
<span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-bell"></i></span> <span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-fw fa-bell"></i></span>
% if notifier['friendly_name']: % if notifier['friendly_name']:
${notifier['agent_label']} &nbsp;<span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span> ${notifier['agent_label']} &nbsp;<span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
% else: % else:
${notifier['agent_label']} &nbsp;<span class="friendly_name">(${notifier['id']})</span> ${notifier['agent_label']} &nbsp;<span class="friendly_name">(${notifier['id']})</span>
% endif % endif
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
</span> </span>
</li> </li>
% endfor % endfor
<li class="add-notification-agent" id="add-notification-agent" data-target="#add-notifier-modal" data-toggle="modal"> <li class="add-notification-agent pointer" id="add-notification-agent" data-target="#add-notifier-modal" data-toggle="modal">
<span> <span>
<span class="toggle-left"><i class="fa fa-lg fa-bell"></i></span> Add a new notification agent <span class="toggle-left"><i class="fa fa-lg fa-fw fa-bell"></i></span> Add a new notification agent
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-plus"></i></span>
</span> </span>
</li> </li>
</ul> </ul>

View File

@@ -115,9 +115,9 @@
</div> </div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
<label> <label>
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Successive Play History <input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Play History
</label> </label>
<p class="help-block">Group successive play history by the same user as a single entry in the watch statistics, tables, and graphs.</p> <p class="help-block">Group play history for the same item and user as a single entry when progress is less than the watched percent.</p>
</div> </div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
<label> <label>
@@ -646,11 +646,11 @@
<div role="tabpanel" class="tab-pane" id="tabs-plex_media_server"> <div role="tabpanel" class="tab-pane" id="tabs-plex_media_server">
<div class="padded-header"> <div class="padded-header">
<h3>Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3> <h3 id="resources-xml">Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
</div> </div>
<div class="form-group has-feedback" id="pms_ip_group"> <div class="form-group has-feedback" id="pms_ip_group">
<label for="pms_ip">Plex IP or Hostname</label> <label for="pms_ip">Plex IP Address or Hostname</label>
<div class="row"> <div class="row">
<div class="col-md-9" id="selectize-pms-ip-container"> <div class="col-md-9" id="selectize-pms-ip-container">
<div class="input-group"> <div class="input-group">
@@ -965,10 +965,35 @@
<p class="help-block">Enable to host newsletters on your own domain. This will generate a link to an HTML page where you can view the newsletter.</p> <p class="help-block">Enable to host newsletters on your own domain. This will generate a link to an HTML page where you can view the newsletter.</p>
</div> </div>
<div id="self_host_newsletter_options" style="overlfow: hidden; display: ${'block' if config['newsletter_self_hosted'] == 'checked' else 'none'}"> <div id="self_host_newsletter_options" style="overlfow: hidden; display: ${'block' if config['newsletter_self_hosted'] == 'checked' else 'none'}">
<p class="help-block" id="self_host_newsletter_message"> <div class="form-group">
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet. <p class="help-block" id="self_host_newsletter_message">
</p> Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p> </p>
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
</div>
<div class="form-group">
<label for="newsletter_auth">Newsletter Authentication</label>
<div class="row">
<div class="col-md-6">
<select class="form-control" id="newsletter_auth" name="newsletter_auth">
<option value="0" ${'selected' if config['newsletter_auth'] == 0 else ''}>Disabled</option>
<option value="1" ${'selected' if config['newsletter_auth'] == 1 else ''}>Password</option>
<option value="2" ${'selected' if config['newsletter_auth'] == 2 else ''}>Tautulli Guest Access</option>
</select>
</div>
</div>
<p class="help-block">Select the authentication method to use for self-hosted newsletters.</p>
<p class="help-block settings-warning newsletter-guest-access-warning">Warning: Guest Access is not enabled under <a data-tab-destination="tabs-web_interface" data-target="#allow_guest_access">Web Interface</a>.</p>
</div>
<div class="form-group" id="newsletter_password_option">
<label for="newsletter_password">Newsletter Password</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_password" name="newsletter_password" value="${config['newsletter_password']}">
</div>
</div>
<p class="help-block">Enter the password that will be required to view self-hosted newsletters.</p>
</div>
</div> </div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
@@ -1025,10 +1050,12 @@
<p class="help-block">Select where to host Plex images for notifications and newsletters.</p> <p class="help-block">Select where to host Plex images for notifications and newsletters.</p>
</div> </div>
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}"> <div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
<p class="help-block" id="imgur_upload_message"> <div class="form-group">
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br> <p class="help-block" id="imgur_upload_message">
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead. You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
</p> Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="imgur_client_id">Imgur Client ID</label> <label for="imgur_client_id">Imgur Client ID</label>
<div class="row"> <div class="row">
@@ -1040,13 +1067,17 @@
</div> </div>
</div> </div>
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}"> <div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
<p class="help-block" id="self_host_image_message">Note: The <span class="inline-pre">${http_root}image</span> endpoint on your domain must be publicly accessible from the internet.</p> <div class="form-group">
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p> <p class="help-block" id="self_host_image_message">Note: The <span class="inline-pre">${http_root}image</span> endpoint on your domain must be publicly accessible from the internet.</p>
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
</div>
</div> </div>
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}"> <div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
<p class="help-block" id="imgur_upload_message"> <div class="form-group">
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br> <p class="help-block" id="imgur_upload_message">
</p> You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="cloudinary_cloud_name">Cloudinary Cloud Name</label> <label for="cloudinary_cloud_name">Cloudinary Cloud Name</label>
<div class="row"> <div class="row">
@@ -1237,7 +1268,7 @@
</span> </span>
</p> </p>
</div> </div>
<p class="form-group"> <div class="form-group">
<label>Registered Devices</label> <label>Registered Devices</label>
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p> <p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
<p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-web_interface" data-target="#api_enabled">Web Interface</a> to use the app.</p> <p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-web_interface" data-target="#api_enabled">Web Interface</a> to use the app.</p>
@@ -1392,7 +1423,7 @@
<div class="col-md-12"> <div class="col-md-12">
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for agent in sorted(available_notification_agents, key=lambda k: k['label'].lower()): % for agent in sorted(available_notification_agents, key=lambda k: k['label'].lower()):
<li class="new-notification-agent" data-id="${agent['id']}"> <li class="new-notification-agent pointer" data-id="${agent['id']}">
<span>${agent['label']}</span> <span>${agent['label']}</span>
</li> </li>
% endfor % endfor
@@ -1420,7 +1451,7 @@
<div class="col-md-12"> <div class="col-md-12">
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for agent in available_newsletter_agents: % for agent in available_newsletter_agents:
<li class="new-newsletter-agent" data-id="${agent['id']}"> <li class="new-newsletter-agent pointer" data-id="${agent['id']}">
<span>${agent['label']}</span> <span>${agent['label']}</span>
</li> </li>
% endfor % endfor
@@ -1732,9 +1763,6 @@
} else if ("${kwargs.get('reinstall_geoip')}" == 'true') { } else if ("${kwargs.get('reinstall_geoip')}" == 'true') {
$('#reinstall_geoip_db').removeClass('no-highlight').css('color','#e9a049'); $('#reinstall_geoip_db').removeClass('no-highlight').css('color','#e9a049');
} }
if ("${kwargs.get('support')}" == 'true') {
$('.support-modal-link').removeClass('no-highlight').css('color','#e9a049');
}
} }
}); });
} }
@@ -1762,6 +1790,7 @@
} }
function loadNotifierConfig(notifier_id) { function loadNotifierConfig(notifier_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({ $.ajax({
url: 'get_notifier_config_modal', url: 'get_notifier_config_modal',
data: { notifier_id: notifier_id }, data: { notifier_id: notifier_id },
@@ -1769,6 +1798,7 @@
async: true, async: true,
complete: function (xhr, status) { complete: function (xhr, status) {
$("#notifier-config-modal").html(xhr.responseText).modal('show'); $("#notifier-config-modal").html(xhr.responseText).modal('show');
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
} }
}); });
} }
@@ -1785,6 +1815,7 @@
} }
function loadNewsletterConfig(newsletter_id) { function loadNewsletterConfig(newsletter_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({ $.ajax({
url: 'get_newsletter_config_modal', url: 'get_newsletter_config_modal',
data: { newsletter_id: newsletter_id }, data: { newsletter_id: newsletter_id },
@@ -1792,6 +1823,7 @@
async: true, async: true,
complete: function (xhr, status) { complete: function (xhr, status) {
$("#newsletter-config-modal").html(xhr.responseText).modal('show'); $("#newsletter-config-modal").html(xhr.responseText).modal('show');
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
} }
}); });
} }
@@ -1808,6 +1840,7 @@
} }
function loadMobileDeviceConfig(mobile_device_id) { function loadMobileDeviceConfig(mobile_device_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({ $.ajax({
url: 'get_mobile_device_config_modal', url: 'get_mobile_device_config_modal',
data: { mobile_device_id: mobile_device_id }, data: { mobile_device_id: mobile_device_id },
@@ -1815,6 +1848,7 @@
async: true, async: true,
complete: function (xhr, status) { complete: function (xhr, status) {
$("#mobile-device-config-modal").html(xhr.responseText).modal('show'); $("#mobile-device-config-modal").html(xhr.responseText).modal('show');
showMsg('<i class="fa fa-check"></i> Configuration Loaded', false, true, 2000);
} }
}); });
} }
@@ -2321,7 +2355,7 @@ $(document).ready(function() {
data: { pref: 'PublishServerOnPlexOnlineKey' }, data: { pref: 'PublishServerOnPlexOnlineKey' },
async: true, async: true,
success: function(data) { success: function(data) {
if (data !== 'true') { if (data === 'false' || data === '0') {
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help."); $("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
$("#monitor_remote_access").attr("checked", false).attr("disabled", true); $("#monitor_remote_access").attr("checked", false).attr("disabled", true);
} }
@@ -2456,6 +2490,7 @@ $(document).ready(function() {
$("#allow_guest_access").attr("disabled", false); $("#allow_guest_access").attr("disabled", false);
$("#allowGuestCheck").html(""); $("#allowGuestCheck").html("");
} }
newsletterPasswordEnabled();
} }
allowGuestAccessCheck(); allowGuestAccessCheck();
@@ -2559,7 +2594,7 @@ $(document).ready(function() {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = result.message; var msg = result.message;
$('#add-notifier-modal').modal('hide'); $('#add-notifier-modal').modal('hide');
if (result.result == 'success') { if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000); showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
loadNotifierConfig(result.notifier_id); loadNotifierConfig(result.notifier_id);
} else { } else {
@@ -2581,7 +2616,7 @@ $(document).ready(function() {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = result.message; var msg = result.message;
$('#add-newsletter-modal').modal('hide'); $('#add-newsletter-modal').modal('hide');
if (result.result == 'success') { if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000); showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
loadNewsletterConfig(result.newsletter_id); loadNewsletterConfig(result.newsletter_id);
} else { } else {
@@ -2680,6 +2715,28 @@ $(document).ready(function() {
newsletterUploadEnabled(); newsletterUploadEnabled();
}); });
function newsletterPasswordEnabled() {
if ($('#newsletter_auth').val() === '1') {
$('#newsletter_password_option').slideDown();
} else {
$('#newsletter_password_option').slideUp();
}
if ($('#newsletter_auth').val() === '2' && !($('#allow_guest_access').is(':checked'))) {
$('.newsletter-guest-access-warning').show();
} else {
$('.newsletter-guest-access-warning').hide();
}
}
newsletterPasswordEnabled();
$('#newsletter_auth').change(function () {
newsletterPasswordEnabled();
});
$('#allow_guest_access').click(function () {
newsletterPasswordEnabled();
})
$('body').on('click', 'a[data-tab-destination]', function () { $('body').on('click', 'a[data-tab-destination]', function () {
var tab = $(this).data('tab-destination'); var tab = $(this).data('tab-destination');
$("a[href=#" + tab + "]").click(); $("a[href=#" + tab + "]").click();
@@ -2693,6 +2750,10 @@ $(document).ready(function() {
body_container.animate({scrollTop: scroll_pos}); body_container.animate({scrollTop: scroll_pos});
} }
}); });
$('#resources-xml').on('tripleclick', function () {
openPlexXML('/api/resources', true, {includeHttps: 1});
});
}); });
</script> </script>
</%def> </%def>

View File

@@ -46,8 +46,10 @@ DOCUMENTATION :: END
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="info-modal-title"> <h4 class="modal-title" id="info-modal-title">
% if data['media_type'] == 'episode' or data['media_type'] == 'track': % if data['media_type'] == 'episode':
Stream Info: <strong>${data['grandparent_title']} - ${data['title']} (${user})</strong> Stream Info: <strong>${data['grandparent_title']} - ${data['title']} (${user})</strong>
% elif data['media_type'] == 'track':
Stream Info: <strong>${data['original_title'] or data['grandparent_title']} - ${data['title']} (${user})</strong>
% else: % else:
Stream Info: <strong>${data['title']} (${user})</strong> Stream Info: <strong>${data['title']} (${user})</strong>
% endif % endif

View File

@@ -0,0 +1,68 @@
<%inherit file="base.html"/>
<%!
from plexpy.helpers import anon_url
%>
<%def name="headIncludes()">
</%def>
<%def name="headerIncludes()">
</%def>
<%def name="body()">
<div class='container-fluid'>
<div class='table-card-header'>
<div class="header-bar">
<span><i class="fa fa-comment"></i> Support</span>
</div>
<div class="button-bar">
<div class="btn-group">
<a class="btn btn-dark" href="${anon_url('https://tautulli.com/discord')}" target="_blank"><i class="fab fa-discord"></i>&nbsp; Join Discord</a>
</div>
<div class="btn-group">
<a class="btn btn-dark" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank"><i class="fab fa-reddit"></i>&nbsp; Join Reddit</a>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="popout-iframe-button"><i class="fa fa-external-link"></i> Pop Out Chat</button>
</div>
</div>
</div>
<div class='table-card-back'>
<div class="iframe-container">
<iframe class="iframe" allowfullscreen="true" id="support-iframe" data-name="Tautulli-Support" data-src="https://support.tautulli.com"
sandbox="allow-presentation allow-forms allow-same-origin allow-pointer-lock allow-scripts allow-popups allow-modals allow-top-navigation"
style="display: none;">
</iframe>
<div class="iframe-overlay">
<div class="iframe-button-container">
<a class="iframe-button">Start chatting now</a>
</div>
</div>
</div>
</div>
</div>
</%def>
<%def name="modalIncludes()">
</%def>
<%def name="javascriptIncludes()">
<script>
var popout_chat;
$('.iframe-button').click(function () {
if (popout_chat) {
popout_chat.close();
}
var iframe = $('#support-iframe');
iframe.attr('src', iframe.data('src')).fadeIn();
$('.iframe-overlay').fadeOut();
});
$('#popout-iframe-button').click(function () {
var iframe = $('#support-iframe');
popout_chat = window.open(iframe.data('src'), 'Tautulli-Discord-Support', 'width=1280,height=720');
iframe.attr('src', '').fadeOut();
$('.iframe-overlay').fadeIn();
});
</script>
</%def>

View File

@@ -16,7 +16,7 @@
<div class='container-fluid'> <div class='container-fluid'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-cloud-download"></i> Synced Items</span> <span id="sync-xml"><i class="fa fa-cloud-download"></i> Synced Items</span>
</div> </div>
<div class="button-bar"> <div class="button-bar">
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -185,5 +185,9 @@
$("#refresh-syncs-list").click(function() { $("#refresh-syncs-list").click(function() {
sync_table.ajax.reload(); sync_table.ajax.reload();
}); });
$('#sync-xml').on('tripleclick', function () {
openPlexXML('/servers/{machine_id}/sync_lists', true);
});
</script> </script>
</%def> </%def>

View File

@@ -108,8 +108,8 @@ DOCUMENTATION :: END
</div> </div>
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3 title="${item['grandparent_title']}"> <h3 title="${item['original_title'] or item['grandparent_title']}">
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a> <a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['original_title'] or item['grandparent_title']}">${item['original_title'] or item['grandparent_title']}</a>
</h3> </h3>
<h3 class="text-muted" title="${item['title']}"> <h3 class="text-muted" title="${item['title']}">
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a>

View File

@@ -10,7 +10,7 @@
<div class='container-fluid'> <div class='container-fluid'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-group"></i> All Users</span> <span id="users-xml"><i class="fa fa-group"></i> All Users</span>
</div> </div>
<div class="button-bar"> <div class="button-bar">
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -202,5 +202,9 @@
}); });
}); });
% endif % endif
$('#users-xml').on('tripleclick', function () {
openPlexXML('/api/users', true);
});
</script> </script>
</%def> </%def>

View File

@@ -594,7 +594,7 @@
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);"> <table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr> <tr>
<td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;"> <td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;">
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['poster_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);"> <table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr> <tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;"> <td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" style="text-decoration: underline;"> <a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank" style="text-decoration: underline;">
@@ -723,7 +723,7 @@
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);"> <table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr> <tr>
<td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;"> <td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 227px;">
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['poster_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);"> <table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr> <tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;"> <td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" style="text-decoration: underline;"> <a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank" style="text-decoration: underline;">
@@ -866,7 +866,7 @@
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);"> <table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #282828;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr> <tr>
<td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 152px;"> <td class="card-poster-container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;width: 152px;min-width: 152px;height: 152px;">
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['poster_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);"> <table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']});border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;background-color: #3F4245;background-position: center;background-size: cover;background-repeat: no-repeat;background-clip: padding-box;border: 1px solid rgba(255,255,255,.1);">
<tr> <tr>
<td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;"> <td style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;">
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" style="text-decoration: underline;"> <a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank" style="text-decoration: underline;">
@@ -888,7 +888,7 @@
<p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;"> <p class="nowrap mb5" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;margin-bottom: 5px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;max-width: 325px;color: #ffffff;">
<em>${album['parent_title']} &middot; ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em> <em>${album['parent_title']} &middot; ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
</p> </p>
% if artist['title'].lower() != 'various artists': % if album['parent_title'].lower() != 'various artists':
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;"> <p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;max-width: 325px;color: #ffffff;">
${album['summary'][:200] + (album['summary'][200:] and '...')} ${album['summary'][:200] + (album['summary'][200:] and '...')}
</p> </p>
@@ -955,7 +955,7 @@
<td class="footer" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;clear: both;margin-top: 10px;text-align: center;width: 100%;"> <td class="footer" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 12px;vertical-align: top;clear: both;margin-top: 10px;text-align: center;width: 100%;">
<div class="footer-bar" style="margin-left: auto;margin-right: auto;width: 200px;border-top: 1px solid #E5A00D;margin-top: 25px;"></div> <div class="footer-bar" style="margin-left: auto;margin-right: auto;width: 200px;border-top: 1px solid #E5A00D;margin-top: 25px;"></div>
<div class="content-block powered-by" style="padding-bottom: 10px;padding-top: 10px;"> <div class="content-block powered-by" style="padding-bottom: 10px;padding-top: 10px;">
Newsletter generated by <a href="http://tautulli.com" target="_blank" style="text-decoration: underline;color: #fff;font-size: 12px;text-align: center;">Tautulli</a>. <!-- FOOTER MESSAGE - DO NOT REMOVE -->
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -595,7 +595,7 @@
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});"> <table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + movie['art_hash']) if base_url_image else movie['art_url']});">
<tr> <tr>
<td class="card-poster-container"> <td class="card-poster-container">
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['poster_url']})"> <table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + movie['thumb_hash']) if base_url_image else movie['thumb_url']})">
<tr> <tr>
<td> <td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank"> <a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}" target="_blank">
@@ -724,7 +724,7 @@
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});"> <table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + show['art_hash']) if base_url_image else show['art_url']});">
<tr> <tr>
<td class="card-poster-container"> <td class="card-poster-container">
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['poster_url']})"> <table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + show['thumb_hash']) if base_url_image else show['thumb_url']})">
<tr> <tr>
<td> <td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank"> <a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${show['title']}" target="_blank">
@@ -867,7 +867,7 @@
<table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});"> <table border="0" cellpadding="0" cellspacing="3" width="100%" class="card-background" style="background-image: url(${(base_url_image + album['art_hash']) if base_url_image else album['art_url']});">
<tr> <tr>
<td class="card-poster-container"> <td class="card-poster-container">
<table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['poster_url']})"> <table border="0" cellpadding="0" cellspacing="0" class="card-poster" style="background-image: url(${(base_url_image + album['thumb_hash']) if base_url_image else album['thumb_url']})">
<tr> <tr>
<td> <td>
<a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank"> <a href="${parameters['pms_web_url']}#!/server/${parameters['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}" target="_blank">
@@ -889,7 +889,7 @@
<p class="nowrap mb5"> <p class="nowrap mb5">
<em>${album['parent_title']} &middot; ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em> <em>${album['parent_title']} &middot; ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
</p> </p>
% if artist['title'].lower() != 'various artists': % if album['parent_title'].lower() != 'various artists':
<p> <p>
${album['summary'][:200] + (album['summary'][200:] and '...')} ${album['summary'][:200] + (album['summary'][200:] and '...')}
</p> </p>
@@ -956,7 +956,7 @@
<td class="footer"> <td class="footer">
<div class="footer-bar"></div> <div class="footer-bar"></div>
<div class="content-block powered-by"> <div class="content-block powered-by">
Newsletter generated by <a href="http://tautulli.com" target="_blank">Tautulli</a>. <!-- FOOTER MESSAGE - DO NOT REMOVE -->
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -23,7 +23,7 @@ __author__ = 'The Python-Twitter Developers'
__email__ = 'python-twitter@googlegroups.com' __email__ = 'python-twitter@googlegroups.com'
__copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers' __copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers'
__license__ = 'Apache License 2.0' __license__ = 'Apache License 2.0'
__version__ = '3.0rc1' __version__ = '3.4.1'
__url__ = 'https://github.com/bear/python-twitter' __url__ = 'https://github.com/bear/python-twitter'
__download_url__ = 'https://pypi.python.org/pypi/python-twitter' __download_url__ = 'https://pypi.python.org/pypi/python-twitter'
__description__ = 'A Python wrapper around the Twitter API' __description__ = 'A Python wrapper around the Twitter API'

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import errno import errno
import os import os
import re
import tempfile import tempfile
from hashlib import md5 from hashlib import md5
@@ -47,7 +46,7 @@ class _FileCache(object):
path = self._GetPath(key) path = self._GetPath(key)
if not path.startswith(self._root_directory): if not path.startswith(self._root_directory):
raise _FileCacheError('%s does not appear to live under %s' % raise _FileCacheError('%s does not appear to live under %s' %
(path, self._root_directory )) (path, self._root_directory))
if os.path.exists(path): if os.path.exists(path):
os.remove(path) os.remove(path)
@@ -101,61 +100,3 @@ class _FileCache(object):
def _GetPrefix(self, hashed_key): def _GetPrefix(self, hashed_key):
return os.path.sep.join(hashed_key[0:_FileCache.DEPTH]) return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])
class ParseTweet(object):
# compile once on import
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
"HASHTAG": r"(#[\w\d]+)", "URL": r"([http://]?[a-zA-Z\d\/]+[\.]+[a-zA-Z\d\/\.]+)"}
regexp = dict((key, re.compile(value)) for key, value in list(regexp.items()))
def __init__(self, timeline_owner, tweet):
""" timeline_owner : twitter handle of user account. tweet - 140 chars from feed; object does all computation on construction
properties:
RT, MT - boolean
URLs - list of URL
Hashtags - list of tags
"""
self.Owner = timeline_owner
self.tweet = tweet
self.UserHandles = ParseTweet.getUserHandles(tweet)
self.Hashtags = ParseTweet.getHashtags(tweet)
self.URLs = ParseTweet.getURLs(tweet)
self.RT = ParseTweet.getAttributeRT(tweet)
self.MT = ParseTweet.getAttributeMT(tweet)
# additional intelligence
if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet?
self.Owner = self.UserHandles[0]
return
def __str__(self):
""" for display method """
return "owner %s, urls: %d, hashtags %d, user_handles %d, len_tweet %d, RT = %s, MT = %s" % (
self.Owner, len(self.URLs), len(self.Hashtags), len(self.UserHandles),
len(self.tweet), self.RT, self.MT)
@staticmethod
def getAttributeRT(tweet):
""" see if tweet is a RT """
return re.search(ParseTweet.regexp["RT"], tweet.strip()) is not None
@staticmethod
def getAttributeMT(tweet):
""" see if tweet is a MT """
return re.search(ParseTweet.regexp["MT"], tweet.strip()) is not None
@staticmethod
def getUserHandles(tweet):
""" given a tweet we try and extract all user handles in order of occurrence"""
return re.findall(ParseTweet.regexp["ALNUM"], tweet)
@staticmethod
def getHashtags(tweet):
""" return all hashtags"""
return re.findall(ParseTweet.regexp["HASHTAG"], tweet)
@staticmethod
def getURLs(tweet):
""" URL : [http://]?[\w\.?/]+"""
return re.findall(ParseTweet.regexp["URL"], tweet)

File diff suppressed because it is too large Load Diff

View File

@@ -8,3 +8,18 @@ class TwitterError(Exception):
def message(self): def message(self):
'''Returns the first argument used to construct this error.''' '''Returns the first argument used to construct this error.'''
return self.args[0] return self.args[0]
class PythonTwitterDeprecationWarning(DeprecationWarning):
"""Base class for python-twitter deprecation warnings"""
pass
class PythonTwitterDeprecationWarning330(PythonTwitterDeprecationWarning):
"""Warning for features to be removed in version 3.3.0"""
pass
class PythonTwitterDeprecationWarning340(PythonTwitterDeprecationWarning):
"""Warning for features to be removed in version 3.4.0"""
pass

View File

@@ -28,6 +28,13 @@ class TwitterModel(object):
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self):
if hasattr(self, 'id'):
return hash(self.id)
else:
raise TypeError('unhashable type: {} (no id attribute)'
.format(type(self)))
def AsJsonString(self): def AsJsonString(self):
""" Returns the TwitterModel as a JSON string based on key/value """ Returns the TwitterModel as a JSON string based on key/value
pairs returned from the AsDict() method. """ pairs returned from the AsDict() method. """
@@ -78,11 +85,14 @@ class TwitterModel(object):
""" """
json_data = data.copy()
if kwargs: if kwargs:
for key, val in kwargs.items(): for key, val in kwargs.items():
data[key] = val json_data[key] = val
return cls(**data) c = cls(**json_data)
c._json = data
return c
class Media(TwitterModel): class Media(TwitterModel):
@@ -93,11 +103,14 @@ class Media(TwitterModel):
self.param_defaults = { self.param_defaults = {
'display_url': None, 'display_url': None,
'expanded_url': None, 'expanded_url': None,
'ext_alt_text': None,
'id': None, 'id': None,
'media_url': None, 'media_url': None,
'media_url_https': None, 'media_url_https': None,
'sizes': None,
'type': None, 'type': None,
'url': None, 'url': None,
'video_info': None,
} }
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
@@ -172,8 +185,10 @@ class DirectMessage(TwitterModel):
self.param_defaults = { self.param_defaults = {
'created_at': None, 'created_at': None,
'id': None, 'id': None,
'recipient': None,
'recipient_id': None, 'recipient_id': None,
'recipient_screen_name': None, 'recipient_screen_name': None,
'sender': None,
'sender_id': None, 'sender_id': None,
'sender_screen_name': None, 'sender_screen_name': None,
'text': None, 'text': None,
@@ -181,6 +196,10 @@ class DirectMessage(TwitterModel):
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default)) setattr(self, param, kwargs.get(param, default))
if 'sender' in kwargs:
self.sender = User.NewFromJsonDict(kwargs.get('sender', None))
if 'recipient' in kwargs:
self.recipient = User.NewFromJsonDict(kwargs.get('recipient', None))
def __repr__(self): def __repr__(self):
if self.text and len(self.text) > 140: if self.text and len(self.text) > 140:
@@ -206,7 +225,7 @@ class Trend(TwitterModel):
'query': None, 'query': None,
'timestamp': None, 'timestamp': None,
'url': None, 'url': None,
'volume': None, 'tweet_volume': None,
} }
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
@@ -218,6 +237,10 @@ class Trend(TwitterModel):
self.timestamp, self.timestamp,
self.url) self.url)
@property
def volume(self):
return self.tweet_volume
class Hashtag(TwitterModel): class Hashtag(TwitterModel):
@@ -259,12 +282,12 @@ class UserStatus(TwitterModel):
""" A class representing the UserStatus structure. This is an abbreviated """ A class representing the UserStatus structure. This is an abbreviated
form of the twitter.User object. """ form of the twitter.User object. """
connections = {'following': False, _connections = {'following': False,
'followed_by': False, 'followed_by': False,
'following_received': False, 'following_received': False,
'following_requested': False, 'following_requested': False,
'blocking': False, 'blocking': False,
'muting': False} 'muting': False}
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.param_defaults = { self.param_defaults = {
@@ -284,10 +307,19 @@ class UserStatus(TwitterModel):
setattr(self, param, kwargs.get(param, default)) setattr(self, param, kwargs.get(param, default))
if 'connections' in kwargs: if 'connections' in kwargs:
for param in self.connections: for param in self._connections:
if param in kwargs['connections']: if param in kwargs['connections']:
setattr(self, param, True) setattr(self, param, True)
@property
def connections(self):
return {'following': self.following,
'followed_by': self.followed_by,
'following_received': self.following_received,
'following_requested': self.following_requested,
'blocking': self.blocking,
'muting': self.muting}
def __repr__(self): def __repr__(self):
connections = [param for param in self.connections if getattr(self, param)] connections = [param for param in self.connections if getattr(self, param)]
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format( return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
@@ -307,11 +339,14 @@ class User(TwitterModel):
'default_profile': None, 'default_profile': None,
'default_profile_image': None, 'default_profile_image': None,
'description': None, 'description': None,
'email': None,
'favourites_count': None, 'favourites_count': None,
'followers_count': None, 'followers_count': None,
'following': None,
'friends_count': None, 'friends_count': None,
'geo_enabled': None, 'geo_enabled': None,
'id': None, 'id': None,
'id_str': None,
'lang': None, 'lang': None,
'listed_count': None, 'listed_count': None,
'location': None, 'location': None,
@@ -319,12 +354,16 @@ class User(TwitterModel):
'notifications': None, 'notifications': None,
'profile_background_color': None, 'profile_background_color': None,
'profile_background_image_url': None, 'profile_background_image_url': None,
'profile_background_image_url_https': None,
'profile_background_tile': None, 'profile_background_tile': None,
'profile_banner_url': None, 'profile_banner_url': None,
'profile_image_url': None, 'profile_image_url': None,
'profile_image_url_https': None,
'profile_link_color': None, 'profile_link_color': None,
'profile_sidebar_border_color': None,
'profile_sidebar_fill_color': None, 'profile_sidebar_fill_color': None,
'profile_text_color': None, 'profile_text_color': None,
'profile_use_background_image': None,
'protected': None, 'protected': None,
'screen_name': None, 'screen_name': None,
'status': None, 'status': None,
@@ -333,6 +372,8 @@ class User(TwitterModel):
'url': None, 'url': None,
'utc_offset': None, 'utc_offset': None,
'verified': None, 'verified': None,
'withheld_in_countries': None,
'withheld_scope': None,
} }
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
@@ -365,6 +406,7 @@ class Status(TwitterModel):
'current_user_retweet': None, 'current_user_retweet': None,
'favorite_count': None, 'favorite_count': None,
'favorited': None, 'favorited': None,
'full_text': None,
'geo': None, 'geo': None,
'hashtags': None, 'hashtags': None,
'id': None, 'id': None,
@@ -377,6 +419,9 @@ class Status(TwitterModel):
'media': None, 'media': None,
'place': None, 'place': None,
'possibly_sensitive': None, 'possibly_sensitive': None,
'quoted_status': None,
'quoted_status_id': None,
'quoted_status_id_str': None,
'retweet_count': None, 'retweet_count': None,
'retweeted': None, 'retweeted': None,
'retweeted_status': None, 'retweeted_status': None,
@@ -395,6 +440,11 @@ class Status(TwitterModel):
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default)) setattr(self, param, kwargs.get(param, default))
if kwargs.get('full_text', None):
self.tweet_mode = 'extended'
else:
self.tweet_mode = 'compatibility'
@property @property
def created_at_in_seconds(self): def created_at_in_seconds(self):
""" Get the time this status message was posted, in seconds since """ Get the time this status message was posted, in seconds since
@@ -414,17 +464,21 @@ class Status(TwitterModel):
string: A string representation of this twitter.Status instance with string: A string representation of this twitter.Status instance with
the ID of status, username and datetime. the ID of status, username and datetime.
""" """
if self.tweet_mode == 'extended':
text = self.full_text
else:
text = self.text
if self.user: if self.user:
return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format( return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format(
self.id, self.id,
self.user.screen_name, self.user.screen_name,
self.created_at, self.created_at,
self.text) text)
else: else:
return u"Status(ID={0}, Created={1}, Text={2!r})".format( return u"Status(ID={0}, Created={1}, Text={2!r})".format(
self.id, self.id,
self.created_at, self.created_at,
self.text) text)
@classmethod @classmethod
def NewFromJsonDict(cls, data, **kwargs): def NewFromJsonDict(cls, data, **kwargs):
@@ -439,17 +493,25 @@ class Status(TwitterModel):
current_user_retweet = None current_user_retweet = None
hashtags = None hashtags = None
media = None media = None
quoted_status = None
retweeted_status = None retweeted_status = None
urls = None urls = None
user = None user = None
user_mentions = None user_mentions = None
# for loading extended tweets from the streaming API.
if 'extended_tweet' in data:
for k, v in data['extended_tweet'].items():
data[k] = v
if 'user' in data: if 'user' in data:
user = User.NewFromJsonDict(data['user']) user = User.NewFromJsonDict(data['user'])
if 'retweeted_status' in data: if 'retweeted_status' in data:
retweeted_status = Status.NewFromJsonDict(data['retweeted_status']) retweeted_status = Status.NewFromJsonDict(data['retweeted_status'])
if 'current_user_retweet' in data: if 'current_user_retweet' in data:
current_user_retweet = data['current_user_retweet']['id'] current_user_retweet = data['current_user_retweet']['id']
if 'quoted_status' in data:
quoted_status = Status.NewFromJsonDict(data.get('quoted_status'))
if 'entities' in data: if 'entities' in data:
if 'urls' in data['entities']: if 'urls' in data['entities']:
@@ -470,6 +532,7 @@ class Status(TwitterModel):
current_user_retweet=current_user_retweet, current_user_retweet=current_user_retweet,
hashtags=hashtags, hashtags=hashtags,
media=media, media=media,
quoted_status=quoted_status,
retweeted_status=retweeted_status, retweeted_status=retweeted_status,
urls=urls, urls=urls,
user=user, user=user,

View File

@@ -2,6 +2,7 @@
import re import re
class Emoticons: class Emoticons:
POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *", POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *",
":P", ":D", ":d", ":p", ":P", ":D", ":d", ":p",
@@ -27,6 +28,7 @@ class Emoticons:
"[:", ";]" "[:", ";]"
] ]
class ParseTweet(object): class ParseTweet(object):
# compile once on import # compile once on import
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)", regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
@@ -51,7 +53,7 @@ class ParseTweet(object):
self.Emoticon = ParseTweet.getAttributeEmoticon(tweet) self.Emoticon = ParseTweet.getAttributeEmoticon(tweet)
# additional intelligence # additional intelligence
if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet? if (self.RT and len(self.UserHandles) > 0): # change the owner of tweet?
self.Owner = self.UserHandles[0] self.Owner = self.UserHandles[0]
return return
@@ -66,10 +68,10 @@ class ParseTweet(object):
emoji = list() emoji = list()
for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()): for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()):
if tok in Emoticons.POSITIVE: if tok in Emoticons.POSITIVE:
emoji.append( tok ) emoji.append(tok)
continue continue
if tok in Emoticons.NEGATIVE: if tok in Emoticons.NEGATIVE:
emoji.append( tok ) emoji.append(tok)
return emoji return emoji
@staticmethod @staticmethod

View File

@@ -97,6 +97,7 @@ class RateLimit(object):
and a dictionary of limit, remaining, and reset will be returned. and a dictionary of limit, remaining, and reset will be returned.
""" """
self.__dict__['resources'] = {}
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@staticmethod @staticmethod
@@ -117,10 +118,12 @@ class RateLimit(object):
for non_std_endpoint in NON_STANDARD_ENDPOINTS: for non_std_endpoint in NON_STANDARD_ENDPOINTS:
if re.match(non_std_endpoint.regex, resource): if re.match(non_std_endpoint.regex, resource):
return non_std_endpoint.resource return non_std_endpoint.resource
else: return resource
return resource
def set_unknown_limit(self, url, limit, remaining, reset): def set_unknown_limit(self, url, limit, remaining, reset):
return self.set_limit(url, limit, remaining, reset)
def set_limit(self, url, limit, remaining, reset):
""" If a resource family is unknown, add it to the object's """ If a resource family is unknown, add it to the object's
dictionary. This is to deal with new endpoints being added to dictionary. This is to deal with new endpoints being added to
the API, but not necessarily to the information returned by the API, but not necessarily to the information returned by
@@ -146,13 +149,18 @@ class RateLimit(object):
""" """
endpoint = self.url_to_resource(url) endpoint = self.url_to_resource(url)
resource_family = endpoint.split('/')[1] resource_family = endpoint.split('/')[1]
self.__dict__['resources'].update( new_endpoint = {endpoint: {
{resource_family: { "limit": enf_type('limit', int, limit),
endpoint: { "remaining": enf_type('remaining', int, remaining),
"limit": limit, "reset": enf_type('reset', int, reset)
"remaining": remaining, }}
"reset": reset
}}}) if not self.resources.get(resource_family, None):
self.resources[resource_family] = {}
self.__dict__['resources'][resource_family].update(new_endpoint)
return self.get_limit(url)
def get_limit(self, url): def get_limit(self, url):
""" Gets a EndpointRateLimit object for the given url. """ Gets a EndpointRateLimit object for the given url.
@@ -181,35 +189,3 @@ class RateLimit(object):
return EndpointRateLimit(family_rates['limit'], return EndpointRateLimit(family_rates['limit'],
family_rates['remaining'], family_rates['remaining'],
family_rates['reset']) family_rates['reset'])
def set_limit(self, url, limit, remaining, reset):
""" Set an endpoint's rate limits. The data used for each of the
args should come from Twitter's ``x-rate-limit`` headers.
Args:
url (str):
URL of the endpoint being fetched.
limit (int):
Max number of times a user or app can hit the endpoint
before being rate limited.
remaining (int):
Number of times a user or app can access the endpoint
before being rate limited.
reset (int):
Epoch time at which the rate limit window will reset.
"""
endpoint = self.url_to_resource(url)
resource_family = endpoint.split('/')[1]
try:
family_rates = self.resources.get(resource_family).get(endpoint)
except AttributeError:
self.set_unknown_limit(url, limit, remaining, reset)
family_rates = self.resources.get(resource_family).get(endpoint)
family_rates['limit'] = enf_type('limit', int, limit)
family_rates['remaining'] = enf_type('remaining', int, remaining)
family_rates['reset'] = enf_type('reset', int, reset)
return EndpointRateLimit(family_rates['limit'],
family_rates['remaining'],
family_rates['reset'])

View File

@@ -1,13 +1,33 @@
# encoding: utf-8 # encoding: utf-8
from __future__ import unicode_literals
import mimetypes import mimetypes
import os import os
import re import re
import sys
from tempfile import NamedTemporaryFile
from unicodedata import normalize
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
import requests import requests
from tempfile import NamedTemporaryFile
from twitter import TwitterError from twitter import TwitterError
import twitter
if sys.version_info < (3,):
range = xrange
if sys.version_info > (3,):
unicode = str
CHAR_RANGES = [
range(0, 4351),
range(8192, 8205),
range(8208, 8223),
range(8242, 8247)]
TLDS = [ TLDS = [
"ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar",
@@ -138,7 +158,14 @@ TLDS = [
"淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团", "淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团",
"飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"] "飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"]
URL_REGEXP = re.compile(r'(?i)((?:https?://|www\\.)*(?:[\w+-_]+[.])(?:' + r'\b|'.join(TLDS) + r'\b|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))+(?:[:\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*)', re.UNICODE) URL_REGEXP = re.compile((
r'('
r'^(?!(https?://|www\.)?\.|ftps?://|([0-9]+\.){{1,3}}\d+)' # exclude urls that start with "."
r'(?:https?://|www\.)*^(?!.*@)(?:[\w+-_]+[.])' # beginning of url
r'(?:{0}\b' # all tlds
r'(?:[:0-9]))' # port numbers & close off TLDs
r'(?:[\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*' # path/query params
r')').format(r'\b|'.join(TLDS)), re.U | re.I | re.X)
def calc_expected_status_length(status, short_url_length=23): def calc_expected_status_length(status, short_url_length=23):
@@ -153,12 +180,19 @@ def calc_expected_status_length(status, short_url_length=23):
Expected length of the status message as an integer. Expected length of the status message as an integer.
""" """
replaced_chars = 0 status_length = 0
status_length = len(status) if isinstance(status, bytes):
match = re.findall(URL_REGEXP, status) status = unicode(status)
if len(match) >= 1: for word in re.split(r'\s', status):
replaced_chars = len(''.join(match)) if is_url(word):
status_length = status_length - replaced_chars + (short_url_length * len(match)) status_length += short_url_length
else:
for character in word:
if any([ord(normalize("NFC", character)) in char_range for char_range in CHAR_RANGES]):
status_length += 1
else:
status_length += 2
status_length += len(re.findall(r'\s', status))
return status_length return status_length
@@ -171,16 +205,14 @@ def is_url(text):
Returns: Returns:
Boolean of whether the text should be treated as a URL or not. Boolean of whether the text should be treated as a URL or not.
""" """
if re.findall(URL_REGEXP, text): return bool(re.findall(URL_REGEXP, text))
return True
else:
return False
def http_to_file(http): def http_to_file(http):
data_file = NamedTemporaryFile() data_file = NamedTemporaryFile()
req = requests.get(http, stream=True) req = requests.get(http, stream=True)
data_file.write(req.raw.data) for chunk in req.iter_content(chunk_size=1024 * 1024):
data_file.write(chunk)
return data_file return data_file
@@ -200,7 +232,8 @@ def parse_media_file(passed_media):
'image/gif', 'image/gif',
'image/bmp', 'image/bmp',
'image/webp'] 'image/webp']
video_formats = ['video/mp4'] video_formats = ['video/mp4',
'video/quicktime']
# If passed_media is a string, check if it points to a URL, otherwise, # If passed_media is a string, check if it points to a URL, otherwise,
# it should point to local file. Create a reference to a file obj for # it should point to local file. Create a reference to a file obj for
@@ -208,7 +241,7 @@ def parse_media_file(passed_media):
if not hasattr(passed_media, 'read'): if not hasattr(passed_media, 'read'):
if passed_media.startswith('http'): if passed_media.startswith('http'):
data_file = http_to_file(passed_media) data_file = http_to_file(passed_media)
filename = os.path.basename(passed_media) filename = os.path.basename(urlparse(passed_media).path)
else: else:
data_file = open(os.path.realpath(passed_media), 'rb') data_file = open(os.path.realpath(passed_media), 'rb')
filename = os.path.basename(passed_media) filename = os.path.basename(passed_media)
@@ -216,8 +249,8 @@ def parse_media_file(passed_media):
# Otherwise, if a file object was passed in the first place, # Otherwise, if a file object was passed in the first place,
# create the standard reference to media_file (i.e., rename it to fp). # create the standard reference to media_file (i.e., rename it to fp).
else: else:
if passed_media.mode != 'rb': if passed_media.mode not in ['rb', 'rb+', 'w+b']:
raise TwitterError({'message': 'File mode must be "rb".'}) raise TwitterError('File mode must be "rb" or "rb+"')
filename = os.path.basename(passed_media.name) filename = os.path.basename(passed_media.name)
data_file = passed_media data_file = passed_media
@@ -226,16 +259,17 @@ def parse_media_file(passed_media):
try: try:
data_file.seek(0) data_file.seek(0)
except: except Exception as e:
pass pass
media_type = mimetypes.guess_type(os.path.basename(filename))[0] media_type = mimetypes.guess_type(os.path.basename(filename))[0]
if media_type in img_formats and file_size > 5 * 1048576: if media_type is not None:
raise TwitterError({'message': 'Images must be less than 5MB.'}) if media_type in img_formats and file_size > 5 * 1048576:
elif media_type in video_formats and file_size > 15 * 1048576: raise TwitterError({'message': 'Images must be less than 5MB.'})
raise TwitterError({'message': 'Videos must be less than 15MB.'}) elif media_type in video_formats and file_size > 15 * 1048576:
elif media_type not in img_formats and media_type not in video_formats: raise TwitterError({'message': 'Videos must be less than 15MB.'})
raise TwitterError({'message': 'Media type could not be determined.'}) elif media_type not in img_formats and media_type not in video_formats:
raise TwitterError({'message': 'Media type could not be determined.'})
return data_file, filename, file_size, media_type return data_file, filename, file_size, media_type
@@ -263,3 +297,18 @@ def enf_type(field, _type, val):
raise TwitterError({ raise TwitterError({
'message': '"{0}" must be type {1}'.format(field, _type.__name__) 'message': '"{0}" must be type {1}'.format(field, _type.__name__)
}) })
def parse_arg_list(args, attr):
out = []
if isinstance(args, (str, unicode)):
out.append(args)
elif isinstance(args, twitter.User):
out.append(getattr(args, attr))
elif isinstance(args, (list, tuple)):
for item in args:
if isinstance(item, (str, unicode)):
out.append(item)
elif isinstance(item, twitter.User):
out.append(getattr(item, attr))
return ",".join([str(item) for item in out])

View File

@@ -42,6 +42,7 @@ import datafactory
import libraries import libraries
import logger import logger
import mobile_app import mobile_app
import newsletters
import newsletter_handler import newsletter_handler
import notification_handler import notification_handler
import notifiers import notifiers
@@ -202,6 +203,7 @@ def initialize(config_file):
logger.error(u"Could not perform upgrades: %s" % e) logger.error(u"Could not perform upgrades: %s" % e)
# Add notifier configs to logger blacklist # Add notifier configs to logger blacklist
newsletters.blacklist_logger()
notifiers.blacklist_logger() notifiers.blacklist_logger()
mobile_app.blacklist_logger() mobile_app.blacklist_logger()
@@ -516,11 +518,12 @@ def dbcheck():
# sessions table :: This is a temp table that logs currently active sessions # sessions table :: This is a temp table that logs currently active sessions
c_db.execute( c_db.execute(
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, ' 'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, session_id TEXT, '
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, ' 'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, ' 'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
'ip_address TEXT, machine_id TEXT, player TEXT, product TEXT, platform TEXT, title TEXT, parent_title TEXT, ' 'ip_address TEXT, machine_id TEXT, player TEXT, product TEXT, platform TEXT, title TEXT, parent_title TEXT, '
'grandparent_title TEXT, full_title TEXT, media_index INTEGER, parent_media_index INTEGER, ' 'grandparent_title TEXT, original_title TEXT, full_title TEXT, '
'media_index INTEGER, parent_media_index INTEGER, '
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, year INTEGER, ' 'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, year INTEGER, '
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, ' 'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, ' 'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
@@ -540,6 +543,7 @@ def dbcheck():
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, ' 'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, ' 'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
'synced_version INTEGER, synced_version_profile TEXT, ' 'synced_version INTEGER, synced_version_profile TEXT, '
'live INTEGER, live_uuid TEXT, '
'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, watched INTEGER DEFAULT 0, ' 'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, watched INTEGER DEFAULT 0, '
'write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)' 'write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)'
) )
@@ -580,8 +584,9 @@ def dbcheck():
c_db.execute( c_db.execute(
'CREATE TABLE IF NOT EXISTS session_history_metadata (id INTEGER PRIMARY KEY, ' 'CREATE TABLE IF NOT EXISTS session_history_metadata (id INTEGER PRIMARY KEY, '
'rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, ' 'rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
'title TEXT, parent_title TEXT, grandparent_title TEXT, full_title TEXT, media_index INTEGER, ' 'title TEXT, parent_title TEXT, grandparent_title TEXT, original_title TEXT, full_title TEXT, '
'parent_media_index INTEGER, section_id INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, ' 'media_index INTEGER, parent_media_index INTEGER, section_id INTEGER, '
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, '
'art TEXT, media_type TEXT, year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, ' 'art TEXT, media_type TEXT, year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, '
'last_viewed_at INTEGER, content_rating TEXT, summary TEXT, tagline TEXT, rating TEXT, ' 'last_viewed_at INTEGER, content_rating TEXT, summary TEXT, tagline TEXT, rating TEXT, '
'duration INTEGER DEFAULT 0, guid TEXT, directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT, ' 'duration INTEGER DEFAULT 0, guid TEXT, directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT, '
@@ -929,7 +934,7 @@ def dbcheck():
except sqlite3.OperationalError: except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.") logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute( c_db.execute(
'ALTER TABLE sessions ADD COLUMN product INTEGER' 'ALTER TABLE sessions ADD COLUMN product TEXT'
) )
c_db.execute( c_db.execute(
'ALTER TABLE sessions ADD COLUMN optimized_version INTEGER' 'ALTER TABLE sessions ADD COLUMN optimized_version INTEGER'
@@ -1064,6 +1069,36 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN watched INTEGER DEFAULT 0' 'ALTER TABLE sessions ADD COLUMN watched INTEGER DEFAULT 0'
) )
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT live FROM sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN live INTEGER'
)
c_db.execute(
'ALTER TABLE sessions ADD COLUMN live_uuid TEXT'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT session_id FROM sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN session_id TEXT'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT original_title FROM sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN original_title TEXT'
)
# Upgrade session_history table from earlier versions # Upgrade session_history table from earlier versions
try: try:
c_db.execute('SELECT reference_id FROM session_history') c_db.execute('SELECT reference_id FROM session_history')
@@ -1150,6 +1185,15 @@ def dbcheck():
'ALTER TABLE session_history_metadata ADD COLUMN labels TEXT' 'ALTER TABLE session_history_metadata ADD COLUMN labels TEXT'
) )
# Upgrade session_history_metadata table from earlier versions
try:
c_db.execute('SELECT original_title FROM session_history_metadata')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table session_history_metadata.")
c_db.execute(
'ALTER TABLE session_history_metadata ADD COLUMN original_title TEXT'
)
# Upgrade session_history_media_info table from earlier versions # Upgrade session_history_media_info table from earlier versions
try: try:
c_db.execute('SELECT transcode_decision FROM session_history_media_info') c_db.execute('SELECT transcode_decision FROM session_history_media_info')
@@ -1723,8 +1767,8 @@ def dbcheck():
for row in result: for row in result:
img_hash = notification_handler.set_hash_image_info( img_hash = notification_handler.set_hash_image_info(
rating_key=row['rating_key'], width=1000, height=1500, fallback='poster') rating_key=row['rating_key'], width=1000, height=1500, fallback='poster')
data_factory.set_img_info(img_hash=img_hash, imgur_title=row['poster_title'], data_factory.set_img_info(img_hash=img_hash, img_title=row['poster_title'],
imgur_url=row['poster_url'], delete_hash=row['delete_hash'], img_url=row['poster_url'], delete_hash=row['delete_hash'],
service='imgur') service='imgur')
db.action('DROP TABLE poster_urls') db.action('DROP TABLE poster_urls')

View File

@@ -226,7 +226,11 @@ class ActivityHandler(object):
db_session = ap.get_session_by_key(session_key=self.get_session_key()) db_session = ap.get_session_by_key(session_key=self.get_session_key())
this_state = self.timeline['state'] this_state = self.timeline['state']
this_key = str(self.timeline['ratingKey']) this_rating_key = str(self.timeline['ratingKey'])
this_key = self.timeline['key']
# Get the live tv session uuid
this_live_uuid = this_key.split('/')[-1] if this_key.startswith('/livetv/sessions') else None
# If we already have this session in the temp table, check for state changes # If we already have this session in the temp table, check for state changes
if db_session: if db_session:
@@ -235,10 +239,11 @@ class ActivityHandler(object):
func=force_stop_stream, args=[self.get_session_key()], minutes=5) func=force_stop_stream, args=[self.get_session_key()], minutes=5)
last_state = db_session['state'] last_state = db_session['state']
last_key = str(db_session['rating_key']) last_rating_key = str(db_session['rating_key'])
last_live_uuid = db_session['live_uuid']
# Make sure the same item is being played # Make sure the same item is being played
if this_key == last_key: if this_rating_key == last_rating_key or this_live_uuid == last_live_uuid:
# Update the session state and viewOffset # Update the session state and viewOffset
if this_state == 'playing': if this_state == 'playing':
# Update the session in our temp session table # Update the session in our temp session table

View File

@@ -15,18 +15,13 @@
from collections import defaultdict from collections import defaultdict
import json import json
import threading
import time import time
import re
import plexpy import plexpy
import database import database
import datafactory import helpers
import libraries import libraries
import log_reader
import logger import logger
import notification_handler
import notifiers
import pmsconnect import pmsconnect
import users import users
@@ -39,6 +34,7 @@ class ActivityProcessor(object):
def write_session(self, session=None, notify=True): def write_session(self, session=None, notify=True):
if session: if session:
values = {'session_key': session.get('session_key', ''), values = {'session_key': session.get('session_key', ''),
'session_id': session.get('session_id', ''),
'transcode_key': session.get('transcode_key', ''), 'transcode_key': session.get('transcode_key', ''),
'section_id': session.get('section_id', ''), 'section_id': session.get('section_id', ''),
'rating_key': session.get('rating_key', ''), 'rating_key': session.get('rating_key', ''),
@@ -50,6 +46,7 @@ class ActivityProcessor(object):
'title': session.get('title', ''), 'title': session.get('title', ''),
'parent_title': session.get('parent_title', ''), 'parent_title': session.get('parent_title', ''),
'grandparent_title': session.get('grandparent_title', ''), 'grandparent_title': session.get('grandparent_title', ''),
'original_title': session.get('original_title', ''),
'full_title': session.get('full_title', ''), 'full_title': session.get('full_title', ''),
'media_index': session.get('media_index', ''), 'media_index': session.get('media_index', ''),
'parent_media_index': session.get('parent_media_index', ''), 'parent_media_index': session.get('parent_media_index', ''),
@@ -60,6 +57,7 @@ class ActivityProcessor(object):
'friendly_name': session.get('friendly_name', ''), 'friendly_name': session.get('friendly_name', ''),
'ip_address': session.get('ip_address', ''), 'ip_address': session.get('ip_address', ''),
'player': session.get('player', ''), 'player': session.get('player', ''),
'product': session.get('product', ''),
'platform': session.get('platform', ''), 'platform': session.get('platform', ''),
'parent_rating_key': session.get('parent_rating_key', ''), 'parent_rating_key': session.get('parent_rating_key', ''),
'grandparent_rating_key': session.get('grandparent_rating_key', ''), 'grandparent_rating_key': session.get('grandparent_rating_key', ''),
@@ -114,7 +112,9 @@ class ActivityProcessor(object):
'stream_audio_channels': session.get('stream_audio_channels', ''), 'stream_audio_channels': session.get('stream_audio_channels', ''),
'stream_subtitle_decision': session.get('stream_subtitle_decision', ''), 'stream_subtitle_decision': session.get('stream_subtitle_decision', ''),
'stream_subtitle_codec': session.get('stream_subtitle_codec', ''), 'stream_subtitle_codec': session.get('stream_subtitle_codec', ''),
'subtitles': session.get('subtitles', ''), 'subtitles': session.get('subtitles', 0),
'live': session.get('live', 0),
'live_uuid': session.get('live_uuid', ''),
'raw_stream_info': json.dumps(session), 'raw_stream_info': json.dumps(session),
'stopped': int(time.time()) 'stopped': int(time.time())
} }
@@ -180,8 +180,9 @@ class ActivityProcessor(object):
if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'): if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'):
logging_enabled = True logging_enabled = True
else: else:
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. " logger.debug(u"Tautulli ActivityProcessor :: Session %s ratingKey %s not logged. "
u"Media type is '%s'" % (session['rating_key'], session['media_type'])) u"Does not meet logging criteria. Media type is '%s'" %
(session['session_key'], session['rating_key'], session['media_type']))
return session['id'] return session['id']
if str(session['paused_counter']).isdigit(): if str(session['paused_counter']).isdigit():
@@ -193,15 +194,16 @@ class ActivityProcessor(object):
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)): (real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
logging_enabled = False logging_enabled = False
logger.debug(u"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s " logger.debug(u"Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
u"seconds, so we're not logging it." % u"which is less than %s seconds, so we're not logging it." %
(session['rating_key'], str(real_play_time), plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)) (session['session_key'], session['rating_key'], str(real_play_time),
plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
if not is_import and session['media_type'] == 'track': if not is_import and session['media_type'] == 'track':
if real_play_time < 15 and session['duration'] >= 30: if real_play_time < 15 and session['duration'] >= 30:
logging_enabled = False logging_enabled = False
logger.debug(u"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs, " logger.debug(u"Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs, "
u"looks like it was skipped so we're not logging it" % u"looks like it was skipped so we're not logging it" %
(session['rating_key'], str(real_play_time))) (session['session_key'], session['rating_key'], str(real_play_time)))
elif is_import and import_ignore_interval: elif is_import and import_ignore_interval:
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
(real_play_time < int(import_ignore_interval)): (real_play_time < int(import_ignore_interval)):
@@ -266,14 +268,15 @@ class ActivityProcessor(object):
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values) self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
# Check if we should group the session, select the last two rows from the user # Check if we should group the session, select the last two rows from the user
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history \ query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history ' \
WHERE user_id = ? ORDER BY id DESC LIMIT 2 ' 'WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 '
args = [session['user_id']] args = [session['user_id'], session['rating_key']]
result = self.db.select(query=query, args=args) result = self.db.select(query=query, args=args)
new_session = prev_session = None new_session = prev_session = None
prev_progress_percent = media_watched_percent = 0
# Get the last insert row id # Get the last insert row id
last_id = self.db.last_insert_id() last_id = self.db.last_insert_id()
@@ -290,11 +293,23 @@ class ActivityProcessor(object):
'user_id': result[1]['user_id'], 'user_id': result[1]['user_id'],
'reference_id': result[1]['reference_id']} 'reference_id': result[1]['reference_id']}
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT
}
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
media_watched_percent = watched_percent.get(session['media_type'], 0)
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? ' query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
# If previous session view offset less than watched percent,
# and new session view offset is greater,
# then set the reference_id to the previous row,
# else set the reference_id to the new id
if prev_session is None and new_session is None: if prev_session is None and new_session is None:
args = [last_id, last_id] args = [last_id, last_id]
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']: elif prev_progress_percent < media_watched_percent and \
prev_session['view_offset'] <= new_session['view_offset']:
args = [prev_session['reference_id'], new_session['id']] args = [prev_session['reference_id'], new_session['id']]
else: else:
args = [new_session['id'], new_session['id']] args = [new_session['id'], new_session['id']]
@@ -394,6 +409,7 @@ class ActivityProcessor(object):
'title': session['title'], 'title': session['title'],
'parent_title': session['parent_title'], 'parent_title': session['parent_title'],
'grandparent_title': session['grandparent_title'], 'grandparent_title': session['grandparent_title'],
'original_title': session['original_title'],
'full_title': session['full_title'], 'full_title': session['full_title'],
'media_index': metadata['media_index'], 'media_index': metadata['media_index'],
'parent_media_index': metadata['parent_media_index'], 'parent_media_index': metadata['parent_media_index'],
@@ -451,6 +467,16 @@ class ActivityProcessor(object):
return None return None
def get_session_by_id(self, session_id=None):
if session_id:
session = self.db.select_single('SELECT * FROM sessions '
'WHERE session_id = ? ',
args=[session_id])
if session:
return session
return None
def set_session_state(self, session_key=None, state=None, **kwargs): def set_session_state(self, session_key=None, state=None, **kwargs):
if str(session_key).isdigit(): if str(session_key).isdigit():
values = {} values = {}

View File

@@ -611,6 +611,7 @@ General optional parameters:
# if we fail to generate the output fake an error # if we fail to generate the output fake an error
except Exception as e: except Exception as e:
logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc()) logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc())
cherrypy.response.status = 500
out['message'] = traceback.format_exc() out['message'] = traceback.format_exc()
out['result'] = 'error' out['result'] = 'error'
@@ -620,6 +621,7 @@ General optional parameters:
out = xmltodict.unparse(out, pretty=True) out = xmltodict.unparse(out, pretty=True)
except Exception as e: except Exception as e:
logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result') logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result')
cherrypy.response.status = 500
try: try:
out['message'] = e out['message'] = e
out['result'] = 'error' out['result'] = 'error'
@@ -660,6 +662,7 @@ General optional parameters:
result = call(**self._api_kwargs) result = call(**self._api_kwargs)
except Exception as e: except Exception as e:
logger.api_error(u'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e)) logger.api_error(u'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
cherrypy.response.status = 400
if self._api_debug: if self._api_debug:
cherrypy.request.show_tracebacks = True cherrypy.request.show_tracebacks = True
# Reraise the exception so the traceback hits the browser # Reraise the exception so the traceback hits the browser
@@ -704,4 +707,7 @@ General optional parameters:
if ret.get('result'): if ret.get('result'):
self._api_result_type = ret.pop('result', None) self._api_result_type = ret.pop('result', None)
if self._api_result_type == 'error':
cherrypy.response.status = 500
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret)) return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))

View File

@@ -23,6 +23,7 @@ PLATFORM = platform.system()
PLATFORM_RELEASE = platform.release() PLATFORM_RELEASE = platform.release()
PLATFORM_VERSION = platform.version() PLATFORM_VERSION = platform.version()
PLATFORM_LINUX_DISTRO = ' '.join(x for x in platform.linux_distribution() if x) PLATFORM_LINUX_DISTRO = ' '.join(x for x in platform.linux_distribution() if x)
PLATFORM_DEVICE_NAME = platform.node()
BRANCH = version.PLEXPY_BRANCH BRANCH = version.PLEXPY_BRANCH
RELEASE = version.PLEXPY_RELEASE_VERSION RELEASE = version.PLEXPY_RELEASE_VERSION
@@ -33,9 +34,9 @@ DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png" DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
DEFAULT_ART = "interfaces/default/images/art.png" DEFAULT_ART = "interfaces/default/images/art.png"
ONLINE_POSTER_THUMB = "http://tautulli.com/images/poster.png" ONLINE_POSTER_THUMB = "https://tautulli.com/images/poster.png"
ONLINE_COVER_THUMB = "http://tautulli.com/images/cover.png" ONLINE_COVER_THUMB = "https://tautulli.com/images/cover.png"
ONLINE_ART = "http://tautulli.com/images/art.png" ONLINE_ART = "https://tautulli.com/images/art.png"
MEDIA_TYPE_HEADERS = { MEDIA_TYPE_HEADERS = {
'movie': 'Movies', 'movie': 'Movies',
@@ -306,10 +307,17 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'}, {'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'}, {'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'}, {'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
{'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year when the notfication was triggered.'}, {'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year when the notfication is triggered.'},
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification was triggered.'}, {'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month when the notfication is triggered.', 'example': '1 to 12'},
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification was triggered.'}, {'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day when the notfication is triggered.', 'example': '1 to 31'},
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification was triggered.'}, {'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour when the notfication is triggered.', 'example': '0 to 23'},
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute when the notfication is triggered.', 'example': '0 to 59'},
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second when the notfication is triggered.', 'example': '0 to 59'},
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday when the notfication is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number when the notfication is triggered.', 'example': '1 to 52'},
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'},
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'},
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'},
] ]
}, },
{ {
@@ -339,6 +347,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version', 'description': 'If the stream is an optimized version.', 'example': '0 or 1'}, {'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version', 'description': 'If the stream is an optimized version.', 'example': '0 or 1'},
{'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', 'description': 'The optimized version profile of the stream.'}, {'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', 'description': 'The optimized version profile of the stream.'},
{'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', 'description': 'If the stream is an synced version.', 'example': '0 or 1'}, {'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', 'description': 'If the stream is an synced version.', 'example': '0 or 1'},
{'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.', 'example': '0 or 1'},
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'}, {'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'},
{'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', 'description': 'The network location of the stream.', 'example': 'lan or wan'}, {'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', 'description': 'The network location of the stream.', 'example': 'lan or wan'},
{'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The required bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'}, {'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The required bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'},
@@ -394,7 +403,7 @@ NOTIFICATION_PARAMETERS = [
{ {
'category': 'Source Metadata Details', 'category': 'Source Metadata Details',
'parameters': [ 'parameters': [
{'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', 'example': 'movie, show, season, episode, artist, album, track'}, {'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', 'example': 'movie, show, season, episode, artist, album, track, clip'},
{'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'}, {'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'},
{'name': 'Library Name', 'type': 'str', 'value': 'library_name', 'description': 'The library name of the item.'}, {'name': 'Library Name', 'type': 'str', 'value': 'library_name', 'description': 'The library name of the item.'},
{'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'}, {'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'},
@@ -402,6 +411,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'}, {'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'},
{'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'}, {'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'},
{'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'}, {'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'},
{'name': 'Track Artist', 'type': 'str', 'value': 'track_artist', 'description': 'The name of the artist of the track.'},
{'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.', 'example': 'e.g. 1, or 1-3'}, {'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.', 'example': 'e.g. 1, or 1-3'},
{'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00', 'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'}, {'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00', 'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'},
{'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', 'example': 'e.g. 6, or 6-10'}, {'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', 'example': 'e.g. 6, or 6-10'},
@@ -473,6 +483,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'}, {'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'},
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'}, {'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'}, {'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'}, {'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'}, {'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'}, {'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
@@ -523,7 +534,14 @@ NEWSLETTER_PARAMETERS = [
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'}, {'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
{'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'}, {'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'},
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'}, {'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'},
{'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year.'}, {'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year of the start date of the newsletter.'},
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'},
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'},
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'},
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'},
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'},
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'},
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'},
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'}, {'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'},
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'}, {'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'},
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'}, {'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'},
@@ -531,6 +549,7 @@ NEWSLETTER_PARAMETERS = [
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'}, {'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'},
{'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', 'description': 'The unique ID number for the newsletter agent.'}, {'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', 'description': 'The unique ID number for the newsletter agent.'},
{'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', 'description': 'The unique ID name for the newsletter agent.'}, {'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', 'description': 'The unique ID name for the newsletter agent.'},
{'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password', 'description': 'The password required to view the newsletter if enabled.'},
] ]
}, },
{ {

View File

@@ -54,6 +54,7 @@ _CONFIG_DEFINITIONS = {
'PMS_TOKEN': (str, 'PMS', ''), 'PMS_TOKEN': (str, 'PMS', ''),
'PMS_SSL': (int, 'PMS', 0), 'PMS_SSL': (int, 'PMS', 0),
'PMS_URL': (str, 'PMS', ''), 'PMS_URL': (str, 'PMS', ''),
'PMS_URL_OVERRIDE': (str, 'PMS', ''),
'PMS_URL_MANUAL': (int, 'PMS', 0), 'PMS_URL_MANUAL': (int, 'PMS', 0),
'PMS_USE_BIF': (int, 'PMS', 0), 'PMS_USE_BIF': (int, 'PMS', 0),
'PMS_UUID': (str, 'PMS', ''), 'PMS_UUID': (str, 'PMS', ''),
@@ -312,6 +313,8 @@ _CONFIG_DEFINITIONS = {
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0), 'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
'MONITORING_INTERVAL': (int, 'Monitoring', 60), 'MONITORING_INTERVAL': (int, 'Monitoring', 60),
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0), 'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
'NEWSLETTER_AUTH': (int, 'Newsletter', 0),
'NEWSLETTER_PASSWORD': (str, 'Newsletter', ''),
'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''), 'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
'NEWSLETTER_INLINE_STYLES': (int, 'Newsletter', 1), 'NEWSLETTER_INLINE_STYLES': (int, 'Newsletter', 1),
'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'), 'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),

View File

@@ -65,7 +65,7 @@ class DataFactory(object):
columns = [ columns = [
'session_history.reference_id', 'session_history.reference_id',
'session_history.id', 'session_history.id',
'started AS date', 'MAX(started) AS date',
'MIN(started) AS started', 'MIN(started) AS started',
'MAX(stopped) AS stopped', 'MAX(stopped) AS stopped',
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \ 'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \
@@ -86,6 +86,7 @@ class DataFactory(object):
'session_history_metadata.title', 'session_history_metadata.title',
'session_history_metadata.parent_title', 'session_history_metadata.parent_title',
'session_history_metadata.grandparent_title', 'session_history_metadata.grandparent_title',
'session_history_metadata.original_title',
'session_history_metadata.year', 'session_history_metadata.year',
'session_history_metadata.media_index', 'session_history_metadata.media_index',
'session_history_metadata.parent_media_index', 'session_history_metadata.parent_media_index',
@@ -132,6 +133,7 @@ class DataFactory(object):
'title', 'title',
'parent_title', 'parent_title',
'grandparent_title', 'grandparent_title',
'original_title',
'year', 'year',
'media_index', 'media_index',
'parent_media_index', 'parent_media_index',
@@ -233,6 +235,7 @@ class DataFactory(object):
'title': item['parent_title'], 'title': item['parent_title'],
'parent_title': item['parent_title'], 'parent_title': item['parent_title'],
'grandparent_title': item['grandparent_title'], 'grandparent_title': item['grandparent_title'],
'original_title': item['original_title'],
'year': item['year'], 'year': item['year'],
'media_index': item['media_index'], 'media_index': item['media_index'],
'parent_media_index': item['parent_media_index'], 'parent_media_index': item['parent_media_index'],
@@ -480,7 +483,8 @@ class DataFactory(object):
elif stat == 'top_music': elif stat == 'top_music':
top_music = [] top_music = []
try: try:
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \ query = 'SELECT t.id, t.grandparent_title, t.original_title, ' \
't.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \ 't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \ 'MAX(t.started) AS last_watch, COUNT(t.id) AS total_plays, SUM(t.d) AS total_duration ' \
'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \ 'FROM (SELECT *, SUM(CASE WHEN stopped > 0 THEN (stopped - started) - ' \
@@ -492,7 +496,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \ ' >= datetime("now", "-%s days", "localtime") ' \
' AND session_history.media_type = "track" ' \ ' AND session_history.media_type = "track" ' \
' GROUP BY %s) AS t ' \ ' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \ 'GROUP BY t.original_title, t.grandparent_title ' \
'ORDER BY %s DESC, started DESC ' \ 'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count) 'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
@@ -501,7 +505,7 @@ class DataFactory(object):
return None return None
for item in result: for item in result:
row = {'title': item['grandparent_title'], row = {'title': item['original_title'] or item['grandparent_title'],
'total_plays': item['total_plays'], 'total_plays': item['total_plays'],
'total_duration': item['total_duration'], 'total_duration': item['total_duration'],
'users_watched': '', 'users_watched': '',
@@ -529,7 +533,8 @@ class DataFactory(object):
elif stat == 'popular_music': elif stat == 'popular_music':
popular_music = [] popular_music = []
try: try:
query = 'SELECT t.id, t.grandparent_title, t.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \ query = 'SELECT t.id, t.grandparent_title, t.original_title, ' \
't.grandparent_rating_key, t.grandparent_thumb, t.section_id, ' \
't.art, t.media_type, t.content_rating, t.labels, t.started, ' \ 't.art, t.media_type, t.content_rating, t.labels, t.started, ' \
'COUNT(DISTINCT t.user_id) AS users_watched, ' \ 'COUNT(DISTINCT t.user_id) AS users_watched, ' \
'MAX(t.started) AS last_watch, COUNT(t.id) as total_plays, SUM(t.d) AS total_duration ' \ 'MAX(t.started) AS last_watch, COUNT(t.id) as total_plays, SUM(t.d) AS total_duration ' \
@@ -542,7 +547,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \ ' >= datetime("now", "-%s days", "localtime") ' \
' AND session_history.media_type = "track" ' \ ' AND session_history.media_type = "track" ' \
' GROUP BY %s) AS t ' \ ' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \ 'GROUP BY t.original_title, t.grandparent_title ' \
'ORDER BY users_watched DESC, %s DESC, started DESC ' \ 'ORDER BY users_watched DESC, %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count) 'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
@@ -551,7 +556,7 @@ class DataFactory(object):
return None return None
for item in result: for item in result:
row = {'title': item['grandparent_title'], row = {'title': item['original_title'] or item['grandparent_title'],
'users_watched': item['users_watched'], 'users_watched': item['users_watched'],
'rating_key': item['grandparent_rating_key'], 'rating_key': item['grandparent_rating_key'],
'last_play': item['last_watch'], 'last_play': item['last_watch'],
@@ -888,7 +893,7 @@ class DataFactory(object):
'video_decision, audio_decision, transcode_decision, width, height, container, ' \ 'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \ 'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \ 'transcode_width, transcode_height, ' \
'session_history_metadata.media_type, title, grandparent_title ' \ 'session_history_metadata.media_type, title, grandparent_title, original_title ' \
'FROM session_history_media_info ' \ 'FROM session_history_media_info ' \
'JOIN session_history ON session_history_media_info.id = session_history.id ' \ 'JOIN session_history ON session_history_media_info.id = session_history.id ' \
'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id ' \ 'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id ' \
@@ -909,7 +914,7 @@ class DataFactory(object):
'video_decision, audio_decision, transcode_decision, width, height, container, ' \ 'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \ 'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \ 'transcode_width, transcode_height, ' \
'media_type, title, grandparent_title ' \ 'media_type, title, grandparent_title, original_title ' \
'FROM sessions ' \ 'FROM sessions ' \
'WHERE session_key = ? %s' % user_cond 'WHERE session_key = ? %s' % user_cond
result = monitor_db.select(query, args=[session_key]) result = monitor_db.select(query, args=[session_key])
@@ -979,6 +984,7 @@ class DataFactory(object):
'media_type': item['media_type'], 'media_type': item['media_type'],
'title': item['title'], 'title': item['title'],
'grandparent_title': item['grandparent_title'], 'grandparent_title': item['grandparent_title'],
'original_title': item['original_title'],
'current_session': 1 if session_key else 0, 'current_session': 1 if session_key else 0,
'pre_tautulli': pre_tautulli 'pre_tautulli': pre_tautulli
} }
@@ -994,7 +1000,8 @@ class DataFactory(object):
'session_history_metadata.rating_key, session_history_metadata.parent_rating_key, ' \ 'session_history_metadata.rating_key, session_history_metadata.parent_rating_key, ' \
'session_history_metadata.grandparent_rating_key, session_history_metadata.title, ' \ 'session_history_metadata.grandparent_rating_key, session_history_metadata.title, ' \
'session_history_metadata.parent_title, session_history_metadata.grandparent_title, ' \ 'session_history_metadata.parent_title, session_history_metadata.grandparent_title, ' \
'session_history_metadata.full_title, library_sections.section_name, ' \ 'session_history_metadata.original_title, session_history_metadata.full_title, ' \
'library_sections.section_name, ' \
'session_history_metadata.media_index, session_history_metadata.parent_media_index, ' \ 'session_history_metadata.media_index, session_history_metadata.parent_media_index, ' \
'session_history_metadata.section_id, session_history_metadata.thumb, ' \ 'session_history_metadata.section_id, session_history_metadata.thumb, ' \
'session_history_metadata.parent_thumb, session_history_metadata.grandparent_thumb, ' \ 'session_history_metadata.parent_thumb, session_history_metadata.grandparent_thumb, ' \
@@ -1043,6 +1050,7 @@ class DataFactory(object):
'parent_rating_key': item['parent_rating_key'], 'parent_rating_key': item['parent_rating_key'],
'grandparent_rating_key': item['grandparent_rating_key'], 'grandparent_rating_key': item['grandparent_rating_key'],
'grandparent_title': item['grandparent_title'], 'grandparent_title': item['grandparent_title'],
'original_title': item['original_title'],
'parent_media_index': item['parent_media_index'], 'parent_media_index': item['parent_media_index'],
'parent_title': item['parent_title'], 'parent_title': item['parent_title'],
'media_index': item['media_index'], 'media_index': item['media_index'],
@@ -1459,7 +1467,7 @@ class DataFactory(object):
result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'), result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'),
args=[item['parent_rating_key']]) args=[item['parent_rating_key']])
for item in result: for item in result:
key = item['media_index'] key = item['media_index'] if item['media_index'] else item['title']
children.update({key: {'rating_key': item['rating_key']}}) children.update({key: {'rating_key': item['rating_key']}})
key = item['parent_media_index'] if match_type == 'index' else item['parent_title'] key = item['parent_media_index'] if match_type == 'index' else item['parent_title']
@@ -1550,8 +1558,11 @@ class DataFactory(object):
if metadata: if metadata:
# Create full_title # Create full_title
if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track': if metadata['media_type'] == 'episode':
full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title']) full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
elif metadata['media_type'] == 'track':
full_title = '%s - %s' % (metadata['title'],
metadata['original_title'] or metadata['grandparent_title'])
else: else:
full_title = metadata['title'] full_title = metadata['title']
@@ -1566,7 +1577,8 @@ class DataFactory(object):
# Update the session_history_metadata table # Update the session_history_metadata table
query = 'UPDATE session_history_metadata SET rating_key = ?, parent_rating_key = ?, ' \ query = 'UPDATE session_history_metadata SET rating_key = ?, parent_rating_key = ?, ' \
'grandparent_rating_key = ?, title = ?, parent_title = ?, grandparent_title = ?, full_title = ?, ' \ 'grandparent_rating_key = ?, title = ?, parent_title = ?, grandparent_title = ?, ' \
'original_title = ?, full_title = ?, ' \
'media_index = ?, parent_media_index = ?, section_id = ?, thumb = ?, parent_thumb = ?, ' \ 'media_index = ?, parent_media_index = ?, section_id = ?, thumb = ?, parent_thumb = ?, ' \
'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \ 'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \
'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \ 'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \
@@ -1575,7 +1587,8 @@ class DataFactory(object):
'WHERE rating_key = ?' 'WHERE rating_key = ?'
args = [metadata['rating_key'], metadata['parent_rating_key'], metadata['grandparent_rating_key'], args = [metadata['rating_key'], metadata['parent_rating_key'], metadata['grandparent_rating_key'],
metadata['title'], metadata['parent_title'], metadata['grandparent_title'], full_title, metadata['title'], metadata['parent_title'], metadata['grandparent_title'],
metadata['original_title'], full_title,
metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'], metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'],
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'], metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'], metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],

View File

@@ -50,7 +50,9 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \ 'FROM (SELECT * FROM session_history ' \
'GROUP BY date(started, "unixepoch", "localtime"), %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY date_played ' \ 'GROUP BY date_played ' \
'ORDER BY started ASC' % (group_by, time_range, user_cond) 'ORDER BY started ASC' % (group_by, time_range, user_cond)
@@ -147,7 +149,9 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \ 'FROM (SELECT * FROM session_history ' \
'GROUP BY strftime("%%w", datetime(started, "unixepoch", "localtime")), %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY dayofweek ' \ 'GROUP BY dayofweek ' \
'ORDER BY daynumber' % (group_by, time_range, user_cond) 'ORDER BY daynumber' % (group_by, time_range, user_cond)
@@ -245,7 +249,9 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \ 'FROM (SELECT * FROM session_history ' \
'GROUP BY strftime("%%H", datetime(started, "unixepoch", "localtime")) , %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY hourofday ' \ 'GROUP BY hourofday ' \
'ORDER BY hourofday' % (group_by, time_range, user_cond) 'ORDER BY hourofday' % (group_by, time_range, user_cond)
@@ -335,7 +341,9 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \ 'FROM (SELECT * FROM session_history ' \
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")), %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \ 'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range) 'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
@@ -591,7 +599,9 @@ class Graphs(object):
'THEN 1 ELSE 0 END) AS ds_count, ' \ 'THEN 1 ELSE 0 END) AS ds_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count ' \ 'THEN 1 ELSE 0 END) AS tc_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \ 'FROM (SELECT * FROM session_history ' \
'GROUP BY date(session_history.started, "unixepoch", "localtime"), %s) ' \
'AS session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \ 'datetime("now", "-%s days", "localtime")) AND ' \

View File

@@ -209,6 +209,9 @@ def now():
now = datetime.datetime.now() now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S") return now.strftime("%Y-%m-%d %H:%M:%S")
def utc_now_iso():
utcnow = datetime.datetime.utcnow()
return utcnow.isoformat()
def human_duration(s, sig='dhms'): def human_duration(s, sig='dhms'):
@@ -835,7 +838,10 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
) )
img_options = {'format': img_format, img_options = {'format': img_format,
'version': int(time.time())} 'fetch_format': 'auto',
'quality': 'auto',
'version': int(time.time()),
'secure': True}
if width != 1000: if width != 1000:
img_options['width'] = str(width) img_options['width'] = str(width)
@@ -947,7 +953,7 @@ def parse_condition_logic_string(s, num_cond=0):
""" """
valid_tokens = re.compile(r'(\(|\)|and|or)') valid_tokens = re.compile(r'(\(|\)|and|or)')
conditions_pattern = re.compile(r'{\d+}') conditions_pattern = re.compile(r'{\d+}')
tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()] tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()]
stack = [[]] stack = [[]]
@@ -958,7 +964,7 @@ def parse_condition_logic_string(s, num_cond=0):
close_bracket_next = False close_bracket_next = False
nest_and = 0 nest_and = 0
nest_nest_and = 0 nest_nest_and = 0
for i, x in enumerate(tokens): for i, x in enumerate(tokens):
if open_bracket_next and x == '(': if open_bracket_next and x == '(':
stack[-1].append([]) stack[-1].append([])
@@ -969,7 +975,7 @@ def parse_condition_logic_string(s, num_cond=0):
close_bracket_next = False close_bracket_next = False
if nest_and: if nest_and:
nest_nest_and += 1 nest_nest_and += 1
elif close_bracket_next and x == ')': elif close_bracket_next and x == ')':
stack.pop() stack.pop()
if not stack: if not stack:
@@ -998,7 +1004,7 @@ def parse_condition_logic_string(s, num_cond=0):
if nest_and > nest_nest_and: if nest_and > nest_nest_and:
stack.pop() stack.pop()
nest_and -= 1 nest_and -= 1
elif bool_next and x == 'and' and i < len(tokens)-1: elif bool_next and x == 'and' and i < len(tokens)-1:
stack[-1].append([]) stack[-1].append([])
stack.append(stack[-1][-1]) stack.append(stack[-1][-1])
@@ -1009,7 +1015,7 @@ def parse_condition_logic_string(s, num_cond=0):
open_bracket_next = True open_bracket_next = True
close_bracket_next = False close_bracket_next = False
nest_and += 1 nest_and += 1
elif bool_next and x == 'or' and i < len(tokens)-1: elif bool_next and x == 'or' and i < len(tokens)-1:
stack[-1].append(x) stack[-1].append(x)
cond_next = True cond_next = True

View File

@@ -39,12 +39,13 @@ class HTTPHandler(object):
else: else:
self.urls = urls self.urls = urls
self.headers = {'X-Plex-Device-Name': 'Tautulli', self.headers = {'X-Plex-Product': 'Tautulli',
'X-Plex-Product': 'Tautulli',
'X-Plex-Version': plexpy.common.RELEASE, 'X-Plex-Version': plexpy.common.RELEASE,
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
'X-Plex-Platform': plexpy.common.PLATFORM, 'X-Plex-Platform': plexpy.common.PLATFORM,
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE, 'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID, 'X-Plex-Device': 'Web',
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
} }
self.token = token self.token = token
@@ -178,5 +179,5 @@ class HTTPHandler(object):
return output return output
except Exception as e: except Exception as e:
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.response_type, e)) logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.output_format, e))
return None return None

View File

@@ -862,13 +862,13 @@ class Libraries(object):
if str(section_id).isdigit(): if str(section_id).isdigit():
query = 'SELECT session_history.id, session_history.media_type, ' \ query = 'SELECT session_history.id, session_history.media_type, ' \
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \ 'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \ 'title, parent_title, grandparent_title, original_title, ' \
'thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
'year, started, user, content_rating, labels, section_id ' \ 'year, started, user, content_rating, labels, section_id ' \
'FROM session_history_metadata ' \ 'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE section_id = ? ' \ 'WHERE section_id = ? ' \
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \ 'GROUP BY session_history.rating_key ' \
' ELSE session_history.rating_key END) ' \
'ORDER BY started DESC LIMIT ?' 'ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[section_id, limit]) result = monitor_db.select(query, args=[section_id, limit])
else: else:
@@ -893,6 +893,7 @@ class Libraries(object):
'title': row['title'], 'title': row['title'],
'parent_title': row['parent_title'], 'parent_title': row['parent_title'],
'grandparent_title': row['grandparent_title'], 'grandparent_title': row['grandparent_title'],
'original_title': row['original_title'],
'thumb': thumb, 'thumb': thumb,
'media_index': row['media_index'], 'media_index': row['media_index'],
'parent_media_index': row['parent_media_index'], 'parent_media_index': row['parent_media_index'],

View File

@@ -28,6 +28,7 @@ import traceback
import plexpy import plexpy
import helpers import helpers
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
# These settings are for file logging only # These settings are for file logging only
FILENAME = "tautulli.log" FILENAME = "tautulli.log"
@@ -48,6 +49,20 @@ logger_plex_websocket = logging.getLogger("plex_websocket")
# Global queue for multiprocessing logging # Global queue for multiprocessing logging
queue = None queue = None
def blacklist_config(config):
blacklist = set()
blacklist_keys = ['HOOK', 'APIKEY', 'KEY', 'PASSWORD', 'TOKEN']
for key, value in config.iteritems():
if isinstance(value, basestring) and len(value.strip()) > 5 and \
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
blacklist.add(value.strip())
_BLACKLIST_WORDS.update(blacklist)
class NoThreadFilter(logging.Filter): class NoThreadFilter(logging.Filter):
""" """
Log filter for the current thread Log filter for the current thread

View File

@@ -138,7 +138,5 @@ def set_last_seen(device_token=None):
def blacklist_logger(): def blacklist_logger():
devices = get_mobile_devices() devices = get_mobile_devices()
for d in devices:
blacklist = set(d['device_token'] for d in devices) logger.blacklist_config(d)
logger._BLACKLIST_WORDS.update(blacklist)

View File

@@ -19,6 +19,7 @@ from itertools import groupby
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from mako import exceptions from mako import exceptions
import os import os
import re
import plexpy import plexpy
import common import common
@@ -195,6 +196,7 @@ def add_newsletter_config(agent_id=None, **kwargs):
newsletter_id = db.last_insert_id() newsletter_id = db.last_insert_id()
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)." logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
% (agent['label'], newsletter_id)) % (agent['label'], newsletter_id))
blacklist_logger()
return newsletter_id return newsletter_id
except Exception as e: except Exception as e:
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e) logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
@@ -205,7 +207,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
if str(agent_id).isdigit(): if str(agent_id).isdigit():
agent_id = int(agent_id) agent_id = int(agent_id)
else: else:
logger.error(u"Tautulli Newsletters :: Unable to set exisiting newsletter: invalid agent_id %s." logger.error(u"Tautulli Newsletters :: Unable to set existing newsletter: invalid agent_id %s."
% agent_id) % agent_id)
return False return False
@@ -253,6 +255,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)." logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
% (agent['label'], newsletter_id)) % (agent['label'], newsletter_id))
newsletter_handler.schedule_newsletters(newsletter_id=newsletter_id) newsletter_handler.schedule_newsletters(newsletter_id=newsletter_id)
blacklist_logger()
return True return True
except Exception as e: except Exception as e:
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e) logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
@@ -273,6 +276,17 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.") logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
def blacklist_logger():
db = database.MonitorDatabase()
notifiers = db.select('SELECT newsletter_config, email_config FROM newsletters')
for n in notifiers:
config = json.loads(n['newsletter_config'] or '{}')
logger.blacklist_config(config)
email_config = json.loads(n['email_config'] or '{}')
logger.blacklist_config(email_config)
def serve_template(templatename, **kwargs): def serve_template(templatename, **kwargs):
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR: if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
@@ -287,9 +301,9 @@ def serve_template(templatename, **kwargs):
try: try:
template = _hplookup.get_template(templatename) template = _hplookup.get_template(templatename)
return template.render(**kwargs) return template.render(**kwargs), False
except: except:
return exceptions.html_error_template().render() return exceptions.html_error_template().render(), True
def generate_newsletter_uuid(): def generate_newsletter_uuid():
@@ -375,6 +389,7 @@ class Newsletter(object):
self.newsletter = None self.newsletter = None
self.is_preview = False self.is_preview = False
self.template_error = None
def set_config(self, config=None, default=None): def set_config(self, config=None, default=None):
return self._validate_config(config=config, default=default) return self._validate_config(config=config, default=default)
@@ -420,7 +435,7 @@ class Newsletter(object):
self.retrieve_data() self.retrieve_data()
return serve_template( newsletter_rendered, self.template_error = serve_template(
templatename=self._TEMPLATE, templatename=self._TEMPLATE,
uuid=self.uuid, uuid=self.uuid,
subject=self.subject_formatted, subject=self.subject_formatted,
@@ -431,9 +446,36 @@ class Newsletter(object):
preview=self.is_preview preview=self.is_preview
) )
if self.template_error:
return newsletter_rendered
# Force Tautulli footer
if '<!-- FOOTER MESSAGE - DO NOT REMOVE -->' in newsletter_rendered:
newsletter_rendered = newsletter_rendered.replace(
'<!-- FOOTER MESSAGE - DO NOT REMOVE -->',
'Newsletter generated by <a href="https://tautulli.com" target="_blank" '
'style="text-decoration: underline;color: inherit;font-size: inherit;">Tautulli</a>.'
)
else:
msg = ('<div style="text-align: center;padding-top: 100px;padding-bottom: 100px;">'
'<p style="font-family: \'Open Sans\', Helvetica, Arial, sans-serif;color: #282A2D;'
'font-size: 18px;line-height: 30px;">'
'The Tautulli newsletter footer was removed from the newsletter template.<br>'
'Please leave the footer in place as it is unobtrusive and supports '
'<a href="https://tautulli.com" target="_blank">Tautulli</a>.<br>Thank you.'
'</p></div>')
newsletter_rendered = re.sub(r'(<body.*?>)', r'\1' + msg, newsletter_rendered)
return newsletter_rendered
def send(self): def send(self):
self.newsletter = self.generate_newsletter() self.newsletter = self.generate_newsletter()
if self.template_error:
logger.error(u"Tautulli Newsletters :: %s newsletter failed to render template. Newsletter not sent." % self.NAME)
return False
if not self._has_data(): if not self._has_data():
logger.warn(u"Tautulli Newsletters :: %s newsletter has no data. Newsletter not sent." % self.NAME) logger.warn(u"Tautulli Newsletters :: %s newsletter has no data. Newsletter not sent." % self.NAME)
return False return False
@@ -513,6 +555,14 @@ class Newsletter(object):
'server_name': plexpy.CONFIG.PMS_NAME, 'server_name': plexpy.CONFIG.PMS_NAME,
'start_date': self.start_date.format(date_format), 'start_date': self.start_date.format(date_format),
'end_date': self.end_date.format(date_format), 'end_date': self.end_date.format(date_format),
'current_year': self.start_date.year,
'current_month': self.start_date.month,
'current_day': self.start_date.day,
'current_hour': self.start_date.hour,
'current_minute': self.start_date.minute,
'current_second': self.start_date.second,
'current_weekday': self.start_date.isocalendar()[2],
'current_week': self.start_date.isocalendar()[1],
'week_number': self.start_date.isocalendar()[1], 'week_number': self.start_date.isocalendar()[1],
'newsletter_time_frame': self.config['time_frame'], 'newsletter_time_frame': self.config['time_frame'],
'newsletter_time_frame_units': self.config['time_frame_units'], 'newsletter_time_frame_units': self.config['time_frame_units'],
@@ -520,7 +570,8 @@ class Newsletter(object):
'newsletter_static_url': base_url + 'id/' + self.newsletter_id_name, 'newsletter_static_url': base_url + 'id/' + self.newsletter_id_name,
'newsletter_uuid': self.uuid, 'newsletter_uuid': self.uuid,
'newsletter_id': self.newsletter_id, 'newsletter_id': self.newsletter_id,
'newsletter_id_name': self.newsletter_id_name 'newsletter_id_name': self.newsletter_id_name,
'newsletter_password': plexpy.CONFIG.NEWSLETTER_PASSWORD
} }
return parameters return parameters
@@ -762,8 +813,9 @@ class RecentlyAdded(Newsletter):
else: else:
item['art_hash'] = '' item['art_hash'] = ''
item['poster_url'] = '' item['thumb_url'] = ''
item['art_url'] = '' item['art_url'] = ''
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
elif helpers.get_img_service(): elif helpers.get_img_service():
# Upload posters and art to image hosting service # Upload posters and art to image hosting service
@@ -779,7 +831,7 @@ class RecentlyAdded(Newsletter):
img=item['thumb'], rating_key=item['rating_key'], title=item['title'], img=item['thumb'], rating_key=item['rating_key'], title=item['title'],
width=150, height=height, fallback=fallback) width=150, height=height, fallback=fallback)
item['poster_url'] = img_info.get('img_url') or common.ONLINE_POSTER_THUMB item['thumb_url'] = img_info.get('img_url') or common.ONLINE_POSTER_THUMB
img_info = get_img_info( img_info = get_img_info(
img=item['art'], rating_key=item['rating_key'], title=item['title'], img=item['art'], rating_key=item['rating_key'], title=item['title'],
@@ -789,6 +841,15 @@ class RecentlyAdded(Newsletter):
item['thumb_hash'] = '' item['thumb_hash'] = ''
item['art_hash'] = '' item['art_hash'] = ''
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
else:
for item in movies + shows + albums:
item['thumb_hash'] = ''
item['art_hash'] = ''
item['thumb_url'] = ''
item['art_url'] = ''
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
self.data['recently_added'] = recently_added self.data['recently_added'] = recently_added

View File

@@ -169,7 +169,7 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id']) user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
return stream_data['machine_id'] not in user_devices return stream_data['machine_id'] not in user_devices
elif stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode': elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration']) progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
if notify_action == 'on_stop': if notify_action == 'on_stop':
@@ -326,7 +326,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs): def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
logger.info(u"Tautulli NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id) logger.info(u"Tautulli NotificationHandler :: Preparing notification for notifier_id %s." % notifier_id)
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id) notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
@@ -633,6 +633,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
notify_params['parent_title']) notify_params['parent_title'])
else: else:
poster_thumb = '' poster_thumb = ''
poster_key = ''
poster_title = ''
img_service = helpers.get_img_service(include_self=True) img_service = helpers.get_img_service(include_self=True)
if img_service not in (None, 'self-hosted'): if img_service not in (None, 'self-hosted'):
@@ -699,6 +701,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
child_count = 1 child_count = 1
grandchild_count = 1 grandchild_count = 1
now = arrow.now()
now_iso = now.isocalendar()
available_params = { available_params = {
# Global paramaters # Global paramaters
'tautulli_version': common.RELEASE, 'tautulli_version': common.RELEASE,
@@ -713,9 +718,17 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'server_platform': plexpy.CONFIG.PMS_PLATFORM, 'server_platform': plexpy.CONFIG.PMS_PLATFORM,
'server_version': plexpy.CONFIG.PMS_VERSION, 'server_version': plexpy.CONFIG.PMS_VERSION,
'action': notify_action.split('on_')[-1], 'action': notify_action.split('on_')[-1],
'week_number': arrow.now().isocalendar()[1], 'current_year': now.year,
'datestamp': arrow.now().format(date_format), 'current_month': now.month,
'timestamp': arrow.now().format(time_format), 'current_day': now.day,
'current_hour': now.hour,
'current_minute': now.minute,
'current_second': now.second,
'current_weekday': now_iso[2],
'current_week': now_iso[1],
'week_number': now_iso[1], # Keep for backwards compatibility
'datestamp': now.format(date_format),
'timestamp': now.format(time_format),
'unixtime': int(time.time()), 'unixtime': int(time.time()),
# Stream parameters # Stream parameters
'streams': stream_count, 'streams': stream_count,
@@ -742,6 +755,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'optimized_version': notify_params['optimized_version'], 'optimized_version': notify_params['optimized_version'],
'optimized_version_profile': notify_params['optimized_version_profile'], 'optimized_version_profile': notify_params['optimized_version_profile'],
'synced_version': notify_params['synced_version'], 'synced_version': notify_params['synced_version'],
'live': notify_params['live'],
'stream_local': notify_params['local'], 'stream_local': notify_params['local'],
'stream_location': notify_params['location'], 'stream_location': notify_params['location'],
'stream_bandwidth': notify_params['bandwidth'], 'stream_bandwidth': notify_params['bandwidth'],
@@ -802,6 +816,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'artist_name': artist_name, 'artist_name': artist_name,
'album_name': album_name, 'album_name': album_name,
'track_name': track_name, 'track_name': track_name,
'track_artist': notify_params['original_title'] or notify_params['grandparent_title'],
'season_num': season_num, 'season_num': season_num,
'season_num00': season_num00, 'season_num00': season_num00,
'episode_num': episode_num, 'episode_num': episode_num,
@@ -879,6 +894,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'subtitle_language': notify_params['subtitle_language'], 'subtitle_language': notify_params['subtitle_language'],
'subtitle_language_code': notify_params['subtitle_language_code'], 'subtitle_language_code': notify_params['subtitle_language_code'],
'file': notify_params['file'], 'file': notify_params['file'],
'filename': os.path.basename(notify_params['file']),
'file_size': helpers.humanFileSize(notify_params['file_size']), 'file_size': helpers.humanFileSize(notify_params['file_size']),
'indexes': notify_params['indexes'], 'indexes': notify_params['indexes'],
'section_id': notify_params['section_id'], 'section_id': notify_params['section_id'],
@@ -904,6 +920,9 @@ def build_server_notify_params(notify_action=None, **kwargs):
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {})) pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {})) plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
now = arrow.now()
now_iso = now.isocalendar()
available_params = { available_params = {
# Global paramaters # Global paramaters
'tautulli_version': common.RELEASE, 'tautulli_version': common.RELEASE,
@@ -918,8 +937,17 @@ def build_server_notify_params(notify_action=None, **kwargs):
'server_version': plexpy.CONFIG.PMS_VERSION, 'server_version': plexpy.CONFIG.PMS_VERSION,
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER, 'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
'action': notify_action.split('on_')[-1], 'action': notify_action.split('on_')[-1],
'datestamp': arrow.now().format(date_format), 'current_year': now.year,
'timestamp': arrow.now().format(time_format), 'current_month': now.month,
'current_day': now.day,
'current_hour': now.hour,
'current_minute': now.minute,
'current_second': now.second,
'current_weekday': now_iso[2],
'current_week': now_iso[1],
'week_number': now_iso[1], # Keep for backwards compatibility
'datestamp': now.format(date_format),
'timestamp': now.format(time_format),
'unixtime': int(time.time()), 'unixtime': int(time.time()),
# Plex Media Server update parameters # Plex Media Server update parameters
'update_version': pms_download_info['version'], 'update_version': pms_download_info['version'],

View File

@@ -62,7 +62,6 @@ import mobile_app
import pmsconnect import pmsconnect
import request import request
import users import users
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
BROWSER_NOTIFIERS = {} BROWSER_NOTIFIERS = {}
@@ -420,7 +419,7 @@ def get_notifiers(notifier_id=None, notify_action=None):
db = database.MonitorDatabase() db = database.MonitorDatabase()
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s' result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
% (', '.join(notify_actions), where), args=args) % (', '.join(notify_actions), where), args=args)
for item in result: for item in result:
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions])) item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
@@ -509,7 +508,7 @@ def add_notifier_config(agent_id=None, **kwargs):
'agent_name': agent['name'], 'agent_name': agent['name'],
'agent_label': agent['label'], 'agent_label': agent['label'],
'friendly_name': '', 'friendly_name': '',
'notifier_config': json.dumps(get_agent_class(agent_id=agent['id']).config), 'notifier_config': json.dumps(agent_class.config),
'custom_conditions': json.dumps(DEFAULT_CUSTOM_CONDITIONS), 'custom_conditions': json.dumps(DEFAULT_CUSTOM_CONDITIONS),
'custom_conditions_logic': '' 'custom_conditions_logic': ''
} }
@@ -540,7 +539,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
if str(agent_id).isdigit(): if str(agent_id).isdigit():
agent_id = int(agent_id) agent_id = int(agent_id)
else: else:
logger.error(u"Tautulli Notifiers :: Unable to set exisiting notifier: invalid agent_id %s." logger.error(u"Tautulli Notifiers :: Unable to set existing notifier: invalid agent_id %s."
% agent_id) % agent_id)
return False return False
@@ -570,7 +569,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
'agent_name': agent['name'], 'agent_name': agent['name'],
'agent_label': agent['label'], 'agent_label': agent['label'],
'friendly_name': kwargs.get('friendly_name', ''), 'friendly_name': kwargs.get('friendly_name', ''),
'notifier_config': json.dumps(notifier_config), 'notifier_config': json.dumps(agent_class.config),
'custom_conditions': kwargs.get('custom_conditions', json.dumps(DEFAULT_CUSTOM_CONDITIONS)), 'custom_conditions': kwargs.get('custom_conditions', json.dumps(DEFAULT_CUSTOM_CONDITIONS)),
'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''), 'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
} }
@@ -612,17 +611,9 @@ def blacklist_logger():
db = database.MonitorDatabase() db = database.MonitorDatabase()
notifiers = db.select('SELECT notifier_config FROM notifiers') notifiers = db.select('SELECT notifier_config FROM notifiers')
blacklist = set()
blacklist_keys = ['hook', 'key', 'password', 'token']
for n in notifiers: for n in notifiers:
config = json.loads(n['notifier_config'] or '{}') config = json.loads(n['notifier_config'] or '{}')
for key, value in config.iteritems(): logger.blacklist_config(config)
if isinstance(value, basestring) and len(value.strip()) > 5 and \
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
blacklist.add(value.strip())
logger._BLACKLIST_WORDS.update(blacklist)
class PrettyMetadata(object): class PrettyMetadata(object):
@@ -661,9 +652,9 @@ class PrettyMetadata(object):
poster_url = self.parameters['poster_url'] poster_url = self.parameters['poster_url']
if not poster_url: if not poster_url:
if self.media_type in ('artist', 'album', 'track'): if self.media_type in ('artist', 'album', 'track'):
poster_url = 'http://tautulli.com/images/cover.png' poster_url = common.ONLINE_COVER_THUMB
else: else:
poster_url = 'http://tautulli.com/images/poster.png' poster_url = common.ONLINE_POSTER_THUMB
return poster_url return poster_url
def get_provider_name(self, provider): def get_provider_name(self, provider):
@@ -682,13 +673,13 @@ class PrettyMetadata(object):
provider_name = 'Trakt.tv' provider_name = 'Trakt.tv'
elif provider == 'lastfm': elif provider == 'lastfm':
provider_name = 'Last.fm' provider_name = 'Last.fm'
else: # else:
if self.media_type == 'movie': # if self.media_type == 'movie':
provider_name = 'IMDb' # provider_name = 'IMDb'
elif self.media_type in ('show', 'season', 'episode'): # elif self.media_type in ('show', 'season', 'episode'):
provider_name = 'TheTVDB' # provider_name = 'TheTVDB'
elif self.media_type in ('artist', 'album', 'track'): # elif self.media_type in ('artist', 'album', 'track'):
provider_name = 'Last.fm' # provider_name = 'Last.fm'
return provider_name return provider_name
def get_provider_link(self, provider=None): def get_provider_link(self, provider=None):
@@ -697,13 +688,13 @@ class PrettyMetadata(object):
provider_link = self.get_plex_url() provider_link = self.get_plex_url()
elif provider: elif provider:
provider_link = self.parameters.get(provider + '_url', '') provider_link = self.parameters.get(provider + '_url', '')
else: # else:
if self.media_type == 'movie': # if self.media_type == 'movie':
provider_link = self.parameters.get('imdb_url', '') # provider_link = self.parameters.get('imdb_url', '')
elif self.media_type in ('show', 'season', 'episode'): # elif self.media_type in ('show', 'season', 'episode'):
provider_link = self.parameters.get('thetvdb_url', '') # provider_link = self.parameters.get('thetvdb_url', '')
elif self.media_type in ('artist', 'album', 'track'): # elif self.media_type in ('artist', 'album', 'track'):
provider_link = self.parameters.get('lastfm_url', '') # provider_link = self.parameters.get('lastfm_url', '')
return provider_link return provider_link
def get_caption(self, provider): def get_caption(self, provider):
@@ -711,6 +702,7 @@ class PrettyMetadata(object):
return 'View on ' + provider_name return 'View on ' + provider_name
def get_title(self, divider='-'): def get_title(self, divider='-'):
title = ''
if self.media_type == 'movie': if self.media_type == 'movie':
title = '%s (%s)' % (self.parameters['title'], self.parameters['year']) title = '%s (%s)' % (self.parameters['title'], self.parameters['year'])
elif self.media_type == 'show': elif self.media_type == 'show':
@@ -728,7 +720,7 @@ class PrettyMetadata(object):
elif self.media_type == 'album': elif self.media_type == 'album':
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name']) title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
elif self.media_type == 'track': elif self.media_type == 'track':
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['track_name']) title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist'])
return title.encode("utf-8") return title.encode("utf-8")
def get_description(self): def get_description(self):
@@ -807,7 +799,7 @@ class Notifier(object):
if response is not None and response.status_code >= 400 and response.status_code < 500: if response is not None and response.status_code >= 400 and response.status_code < 500:
verify_msg = " Verify you notification agent settings are correct." verify_msg = " Verify you notification agent settings are correct."
logger.error(u"Tautulli Notifiers :: {name} notification failed.{}".format(verify_msg, name=self.NAME)) logger.error(u"Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME))
if err_msg: if err_msg:
logger.error(u"Tautulli Notifiers :: {}".format(err_msg)) logger.error(u"Tautulli Notifiers :: {}".format(err_msg))
@@ -1145,7 +1137,8 @@ class DISCORD(Notifier):
plex_url = pretty_metadata.get_plex_url() plex_url = pretty_metadata.get_plex_url()
# Build Discord post attachment # Build Discord post attachment
attachment = {'title': title attachment = {'title': title,
'timestamp': helpers.utc_now_iso()
} }
if self.config['color']: if self.config['color']:
@@ -1250,7 +1243,7 @@ class DISCORD(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'discord_movie_provider', 'name': 'discord_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -1258,7 +1251,7 @@ class DISCORD(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'discord_tv_provider', 'name': 'discord_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -1266,7 +1259,7 @@ class DISCORD(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'discord_music_provider', 'name': 'discord_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.', 'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -1318,10 +1311,11 @@ class EMAIL(Notifier):
recipients = self.config['to'] + self.config['cc'] + self.config['bcc'] recipients = self.config['to'] + self.config['cc'] + self.config['bcc']
mailserver = None
success = False success = False
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
try: try:
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
mailserver.ehlo() mailserver.ehlo()
if self.config['tls']: if self.config['tls']:
@@ -1332,14 +1326,15 @@ class EMAIL(Notifier):
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password'])) mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
mailserver.sendmail(self.config['from'], recipients, msg.as_string()) mailserver.sendmail(self.config['from'], recipients, msg.as_string())
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
success = True success = True
except Exception as e: except Exception as e:
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e)) logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
finally: finally:
mailserver.quit() if mailserver:
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME)) mailserver.quit()
return success return success
@@ -1464,7 +1459,7 @@ class FACEBOOK(Notifier):
return facebook.auth_url(app_id=app_id, return facebook.auth_url(app_id=app_id,
canvas_url=redirect_uri, canvas_url=redirect_uri,
perms=['user_managed_groups','publish_actions']) perms=['publish_to_groups'])
def _get_credentials(self, code=''): def _get_credentials(self, code=''):
logger.info(u"Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME)) logger.info(u"Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))
@@ -1597,7 +1592,7 @@ class FACEBOOK(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'facebook_movie_provider', 'name': 'facebook_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -1605,7 +1600,7 @@ class FACEBOOK(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'facebook_tv_provider', 'name': 'facebook_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -1613,7 +1608,7 @@ class FACEBOOK(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'facebook_music_provider', 'name': 'facebook_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.', 'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -1933,7 +1928,7 @@ class HIPCHAT(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'hipchat_movie_provider', 'name': 'hipchat_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -1941,7 +1936,7 @@ class HIPCHAT(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'hipchat_tv_provider', 'name': 'hipchat_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -1949,7 +1944,7 @@ class HIPCHAT(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'hipchat_music_provider', 'name': 'hipchat_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.', 'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -2141,7 +2136,7 @@ class JOIN(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'join_movie_provider', 'name': 'join_movie_provider',
'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -2149,7 +2144,7 @@ class JOIN(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'join_tv_provider', 'name': 'join_tv_provider',
'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -2157,7 +2152,7 @@ class JOIN(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'join_music_provider', 'name': 'join_music_provider',
'description': 'Select the source for music links in the notificaation. Leave blank for default.', 'description': 'Select the source for music links in the notification. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -2447,7 +2442,8 @@ class PLEX(Notifier):
else: else:
return request.request_content(url) return request.request_content(url)
def _sendjson(self, host, method, params={}): def _sendjson(self, host, method, params=None):
params = params or {}
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'} headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc' url = host + '/jsonrpc'
@@ -2925,7 +2921,7 @@ class PUSHOVER(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'pushover_movie_provider', 'name': 'pushover_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -2933,7 +2929,7 @@ class PUSHOVER(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'pushover_tv_provider', 'name': 'pushover_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -2941,7 +2937,7 @@ class PUSHOVER(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'pushover_music_provider', 'name': 'pushover_music_provider',
'description': 'Select the source for music links in the notification. Leave blank for default.', 'description': 'Select the source for music links in the notification. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -3300,7 +3296,7 @@ class SLACK(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'slack_movie_provider', 'name': 'slack_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -3308,7 +3304,7 @@ class SLACK(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'slack_tv_provider', 'name': 'slack_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 on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -3316,7 +3312,7 @@ class SLACK(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'slack_music_provider', 'name': 'slack_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.', 'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -3476,7 +3472,7 @@ class TWITTER(Notifier):
poster_url = parameters.get('poster_url','') poster_url = parameters.get('poster_url','')
# Hack to add media type to attachment # Hack to add media type to attachment
if poster_url: if poster_url and not helpers.get_img_service():
poster_url += '.png' poster_url += '.png'
if self.config['incl_subject']: if self.config['incl_subject']:
@@ -3550,7 +3546,8 @@ class XBMC(Notifier):
else: else:
return request.request_content(url) return request.request_content(url)
def _sendjson(self, host, method, params={}): def _sendjson(self, host, method, params=None):
params = params or {}
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'} headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc' url = host + '/jsonrpc'
@@ -3711,7 +3708,7 @@ class ZAPIER(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'zapier_movie_provider', 'name': 'zapier_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -3719,7 +3716,7 @@ class ZAPIER(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'zapier_tv_provider', 'name': 'zapier_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -3727,7 +3724,7 @@ class ZAPIER(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'zapier_music_provider', 'name': 'zapier_music_provider',
'description': 'Select the source for music links in the notification. Leave blank for default.', 'description': 'Select the source for music links in the notification. Leave blank to disable.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }

View File

@@ -49,6 +49,7 @@ def extract_plexivity_xml(xml=None):
grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey') grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey')
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb') grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle') grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
original_title = helpers.get_xml_attr(a, 'originalTitle')
guid = helpers.get_xml_attr(a, 'guid') guid = helpers.get_xml_attr(a, 'guid')
section_id = helpers.get_xml_attr(a, 'librarySectionID') section_id = helpers.get_xml_attr(a, 'librarySectionID')
media_index = helpers.get_xml_attr(a, 'index') media_index = helpers.get_xml_attr(a, 'index')
@@ -180,9 +181,10 @@ def extract_plexivity_xml(xml=None):
'duration': duration, 'duration': duration,
'grandparent_rating_key': grandparent_rating_key, 'grandparent_rating_key': grandparent_rating_key,
'grandparent_thumb': grandparent_thumb, 'grandparent_thumb': grandparent_thumb,
'grandparent_title': grandparent_title,
'parent_title': parent_title,
'title': title, 'title': title,
'parent_title': parent_title,
'grandparent_title': grandparent_title,
'original_title': original_title,
'tagline': tagline, 'tagline': tagline,
'guid': guid, 'guid': guid,
'section_id': section_id, 'section_id': section_id,
@@ -339,6 +341,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
'title': row['title'], 'title': row['title'],
'parent_title': extracted_xml['parent_title'], 'parent_title': extracted_xml['parent_title'],
'grandparent_title': row['grandparent_title'], 'grandparent_title': row['grandparent_title'],
'original_title': extracted_xml['original_title'],
'full_title': row['full_title'], 'full_title': row['full_title'],
'user_id': user_id, 'user_id': user_id,
'user': row['user'], 'user': row['user'],
@@ -380,6 +383,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
'title': row['title'], 'title': row['title'],
'parent_title': extracted_xml['parent_title'], 'parent_title': extracted_xml['parent_title'],
'grandparent_title': row['grandparent_title'], 'grandparent_title': row['grandparent_title'],
'original_title': extracted_xml['original_title'],
'media_index': extracted_xml['media_index'], 'media_index': extracted_xml['media_index'],
'parent_media_index': extracted_xml['parent_media_index'], 'parent_media_index': extracted_xml['parent_media_index'],
'thumb': extracted_xml['thumb'], 'thumb': extracted_xml['thumb'],

View File

@@ -45,6 +45,7 @@ def extract_plexwatch_xml(xml=None):
duration = helpers.get_xml_attr(a, 'duration') duration = helpers.get_xml_attr(a, 'duration')
grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb') grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle') grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
original_title = helpers.get_xml_attr(a, 'originalTitle')
guid = helpers.get_xml_attr(a, 'guid') guid = helpers.get_xml_attr(a, 'guid')
section_id = helpers.get_xml_attr(a, 'librarySectionID') section_id = helpers.get_xml_attr(a, 'librarySectionID')
media_index = helpers.get_xml_attr(a, 'index') media_index = helpers.get_xml_attr(a, 'index')
@@ -172,9 +173,10 @@ def extract_plexwatch_xml(xml=None):
'art': art, 'art': art,
'duration': duration, 'duration': duration,
'grandparent_thumb': grandparent_thumb, 'grandparent_thumb': grandparent_thumb,
'grandparent_title': grandparent_title,
'parent_title': parent_title,
'title': title, 'title': title,
'parent_title': parent_title,
'grandparent_title': grandparent_title,
'original_title': original_title,
'tagline': tagline, 'tagline': tagline,
'guid': guid, 'guid': guid,
'section_id': section_id, 'section_id': section_id,
@@ -332,6 +334,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
'title': row['title'], 'title': row['title'],
'parent_title': extracted_xml['parent_title'], 'parent_title': extracted_xml['parent_title'],
'grandparent_title': row['grandparent_title'], 'grandparent_title': row['grandparent_title'],
'original_title': extracted_xml['original_title'],
'full_title': row['full_title'], 'full_title': row['full_title'],
'user_id': user_id, 'user_id': user_id,
'user': row['user'], 'user': row['user'],
@@ -373,6 +376,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
'title': row['title'], 'title': row['title'],
'parent_title': extracted_xml['parent_title'], 'parent_title': extracted_xml['parent_title'],
'grandparent_title': row['grandparent_title'], 'grandparent_title': row['grandparent_title'],
'original_title': extracted_xml['original_title'],
'media_index': extracted_xml['media_index'], 'media_index': extracted_xml['media_index'],
'parent_media_index': extracted_xml['parent_media_index'], 'parent_media_index': extracted_xml['parent_media_index'],
'thumb': extracted_xml['thumb'], 'thumb': extracted_xml['thumb'],

View File

@@ -19,6 +19,7 @@ import time
import urllib import urllib
import plexpy import plexpy
import activity_processor
import common import common
import helpers import helpers
import http_handler import http_handler
@@ -482,6 +483,7 @@ class PmsConnect(object):
actors = [] actors = []
genres = [] genres = []
labels = [] labels = []
collections = []
if m.getElementsByTagName('Director'): if m.getElementsByTagName('Director'):
for director in m.getElementsByTagName('Director'): for director in m.getElementsByTagName('Director'):
@@ -503,6 +505,10 @@ class PmsConnect(object):
for label in m.getElementsByTagName('Label'): for label in m.getElementsByTagName('Label'):
labels.append(helpers.get_xml_attr(label, 'tag')) labels.append(helpers.get_xml_attr(label, 'tag'))
if m.getElementsByTagName('Collection'):
for collection in m.getElementsByTagName('Collection'):
collections.append(helpers.get_xml_attr(collection, 'tag'))
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'), recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
'section_id': helpers.get_xml_attr(m, 'librarySectionID'), 'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'), 'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
@@ -512,6 +518,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(m, 'title'), 'title': helpers.get_xml_attr(m, 'title'),
'parent_title': helpers.get_xml_attr(m, 'parentTitle'), 'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
'sort_title': helpers.get_xml_attr(m, 'titleSort'), 'sort_title': helpers.get_xml_attr(m, 'titleSort'),
'media_index': helpers.get_xml_attr(m, 'index'), 'media_index': helpers.get_xml_attr(m, 'index'),
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
@@ -539,6 +546,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(m, 'title'), 'full_title': helpers.get_xml_attr(m, 'title'),
'child_count': helpers.get_xml_attr(m, 'childCount') 'child_count': helpers.get_xml_attr(m, 'childCount')
} }
@@ -582,6 +590,8 @@ class PmsConnect(object):
metadata_xml = self.get_metadata(str(rating_key), output_format='xml') metadata_xml = self.get_metadata(str(rating_key), output_format='xml')
elif sync_id: elif sync_id:
metadata_xml = self.get_sync_item(str(sync_id), output_format='xml') metadata_xml = self.get_sync_item(str(sync_id), output_format='xml')
else:
return metadata
try: try:
xml_head = metadata_xml.getElementsByTagName('MediaContainer') xml_head = metadata_xml.getElementsByTagName('MediaContainer')
@@ -661,6 +671,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -708,6 +719,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -752,6 +764,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -797,6 +810,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -840,6 +854,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -884,6 +899,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -920,6 +936,8 @@ class PmsConnect(object):
elif metadata_type == 'track': elif metadata_type == 'track':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey') parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
album_details = self.get_metadata_details(parent_rating_key) album_details = self.get_metadata_details(parent_rating_key)
track_artist = helpers.get_xml_attr(metadata_main, 'originalTitle') or \
helpers.get_xml_attr(metadata_main, 'grandparentTitle')
metadata = {'media_type': metadata_type, metadata = {'media_type': metadata_type,
'section_id': section_id, 'section_id': section_id,
'library_name': library_name, 'library_name': library_name,
@@ -929,6 +947,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -957,8 +976,8 @@ class PmsConnect(object):
'genres': album_details['genres'], 'genres': album_details['genres'],
'labels': album_details['labels'], 'labels': album_details['labels'],
'collections': album_details['collections'], 'collections': album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'),
helpers.get_xml_attr(metadata_main, 'title')), track_artist),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount') 'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
} }
@@ -972,6 +991,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -1016,6 +1036,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -1032,7 +1053,7 @@ class PmsConnect(object):
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'), 'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'), 'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
'art': helpers.get_xml_attr(metadata_main, 'art'), 'art': helpers.get_xml_attr(metadata_main, 'art'),
'banner': photo_album_details['banner'], 'banner': photo_album_details.get('banner', ''),
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'), 'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'), 'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
@@ -1041,10 +1062,10 @@ class PmsConnect(object):
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
'genres': photo_album_details['genres'], 'genres': photo_album_details.get('genres', ''),
'labels': photo_album_details['labels'], 'labels': photo_album_details.get('labels', ''),
'collections': photo_album_details['collections'], 'collections': photo_album_details.get('collections', ''),
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name,
helpers.get_xml_attr(metadata_main, 'title')), helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount') 'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
} }
@@ -1060,6 +1081,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -1105,6 +1127,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -1139,7 +1162,7 @@ class PmsConnect(object):
} }
else: else:
return {} return metadata
if metadata and media_info: if metadata and media_info:
medias = [] medias = []
@@ -1243,7 +1266,7 @@ class PmsConnect(object):
return metadata return metadata
else: else:
return {} return metadata
def get_metadata_children_details(self, rating_key='', get_children=False): def get_metadata_children_details(self, rating_key='', get_children=False):
""" """
@@ -1658,6 +1681,8 @@ class PmsConnect(object):
'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'), 'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'),
'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'), 'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
'synced_version': 1 if sync_id else 0, 'synced_version': 1 if sync_id else 0,
'live': int(helpers.get_xml_attr(session, 'live') == '1'),
'live_uuid': helpers.get_xml_attr(stream_media_info, 'uuid'),
'indexes': int(indexes == 'sd'), 'indexes': int(indexes == 'sd'),
'bif_thumb': bif_thumb, 'bif_thumb': bif_thumb,
'subtitles': 1 if subtitle_id and subtitle_selected else 0 'subtitles': 1 if subtitle_id and subtitle_selected else 0
@@ -1670,9 +1695,7 @@ class PmsConnect(object):
if not helpers.get_xml_attr(session, 'ratingKey').isdigit(): if not helpers.get_xml_attr(session, 'ratingKey').isdigit():
channel_stream = 1 channel_stream = 1
clip_media = session.getElementsByTagName('Media')[0] audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels')
clip_part = clip_media.getElementsByTagName('Part')[0]
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
metadata_details = {'media_type': media_type, metadata_details = {'media_type': media_type,
'section_id': helpers.get_xml_attr(session, 'librarySectionID'), 'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
'library_name': helpers.get_xml_attr(session, 'librarySectionTitle'), 'library_name': helpers.get_xml_attr(session, 'librarySectionTitle'),
@@ -1682,6 +1705,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(session, 'title'), 'title': helpers.get_xml_attr(session, 'title'),
'parent_title': helpers.get_xml_attr(session, 'parentTitle'), 'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(session, 'originalTitle'),
'sort_title': helpers.get_xml_attr(session, 'titleSort'), 'sort_title': helpers.get_xml_attr(session, 'titleSort'),
'media_index': helpers.get_xml_attr(session, 'index'), 'media_index': helpers.get_xml_attr(session, 'index'),
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
@@ -1710,18 +1734,17 @@ class PmsConnect(object):
'genres': [], 'genres': [],
'labels': [], 'labels': [],
'full_title': helpers.get_xml_attr(session, 'title'), 'full_title': helpers.get_xml_attr(session, 'title'),
'container': helpers.get_xml_attr(clip_media, 'container') \ 'container': helpers.get_xml_attr(stream_media_info, 'container') \
or helpers.get_xml_attr(clip_part, 'container'), or helpers.get_xml_attr(stream_media_parts_info, 'container'),
'height': helpers.get_xml_attr(clip_media, 'height'), 'height': helpers.get_xml_attr(stream_media_info, 'height'),
'width': helpers.get_xml_attr(clip_media, 'width'), 'width': helpers.get_xml_attr(stream_media_info, 'width'),
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'), 'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(clip_media, 'videoResolution'), 'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution'),
'audio_codec': helpers.get_xml_attr(clip_media, 'audioCodec'), 'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
'audio_channels': audio_channels, 'audio_channels': audio_channels,
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels), 'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'), 'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'), 'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
'live': int(helpers.get_xml_attr(session, 'live') == '1'),
'extra_type': helpers.get_xml_attr(session, 'extraType'), 'extra_type': helpers.get_xml_attr(session, 'extraType'),
'sub_type': helpers.get_xml_attr(session, 'subtype') 'sub_type': helpers.get_xml_attr(session, 'subtype')
} }
@@ -1790,13 +1813,12 @@ class PmsConnect(object):
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details)) next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
# Overrides for live sessions # Overrides for live sessions
if metadata_details.get('live') and transcode_session: if stream_details['live'] and transcode_session:
stream_details['stream_container_decision'] = 'transcode' stream_details['stream_container_decision'] = 'transcode'
stream_details['stream_container'] = transcode_details['transcode_container'] stream_details['stream_container'] = transcode_details['transcode_container']
video_details['stream_video_decision'] = transcode_details['video_decision'] video_details['stream_video_decision'] = transcode_details['video_decision']
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec'] 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'] audio_details['stream_audio_decision'] = transcode_details['audio_decision']
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec'] stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
@@ -1906,7 +1928,7 @@ class PmsConnect(object):
return session_output return session_output
def terminate_session(self, session_id='', message=''): def terminate_session(self, session_key='', session_id='', message=''):
""" """
Terminates a streaming session. Terminates a streaming session.
@@ -1914,10 +1936,22 @@ class PmsConnect(object):
""" """
message = message or 'The server owner has ended the stream.' message = message or 'The server owner has ended the stream.'
if session_key and not session_id:
ap = activity_processor.ActivityProcessor()
session = ap.get_session_by_key(session_key=session_key)
session_id = session['session_id']
elif session_id and not session_key:
ap = activity_processor.ActivityProcessor()
session = ap.get_session_by_id(session_id=session_id)
session_key = session['session_key']
if session_id: if session_id:
logger.info(u"Tautulli Pmsconnect :: Terminating session %s (session_id %s)." % (session_key, session_id))
result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message)) result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message))
return result return result
else: else:
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session %s. Missing session_id." % session_key)
return False return False
def get_item_children(self, rating_key='', get_grandchildren=False): def get_item_children(self, rating_key='', get_grandchildren=False):
@@ -1994,6 +2028,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(m, 'title'), 'title': helpers.get_xml_attr(m, 'title'),
'parent_title': helpers.get_xml_attr(m, 'parentTitle'), 'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
'sort_title': helpers.get_xml_attr(m, 'titleSort'), 'sort_title': helpers.get_xml_attr(m, 'titleSort'),
'media_index': helpers.get_xml_attr(m, 'index'), 'media_index': helpers.get_xml_attr(m, 'index'),
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
@@ -2311,6 +2346,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(item, 'title'), 'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'), 'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(item, 'originalTitle'),
'sort_title': helpers.get_xml_attr(item, 'titleSort'), 'sort_title': helpers.get_xml_attr(item, 'titleSort'),
'media_index': helpers.get_xml_attr(item, 'index'), 'media_index': helpers.get_xml_attr(item, 'index'),
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'), 'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
@@ -2649,7 +2685,7 @@ class PmsConnect(object):
child_title = helpers.get_xml_attr(item, 'title') child_title = helpers.get_xml_attr(item, 'title')
if child_rating_key: if child_rating_key:
key = int(child_index) key = int(child_index) if child_index else child_title
children.update({key: {'rating_key': int(child_rating_key)}}) children.update({key: {'rating_key': int(child_rating_key)}})
key = int(parent_index) if match_type == 'index' else parent_title key = int(parent_index) if match_type == 'index' else parent_title
@@ -2661,9 +2697,9 @@ class PmsConnect(object):
key = 0 if match_type == 'index' else title key = 0 if match_type == 'index' else title
key_list = {key: {'rating_key': int(rating_key), key_list = {key: {'rating_key': int(rating_key),
'children': parents}, 'children': parents},
'section_id': section_id, 'section_id': section_id,
'library_name': library_name 'library_name': library_name
} }
return key_list return key_list

View File

@@ -201,9 +201,10 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
'grandparent_thumb': common.DEFAULT_POSTER_THUMB, 'grandparent_thumb': common.DEFAULT_POSTER_THUMB,
'thumb': common.DEFAULT_POSTER_THUMB, 'thumb': common.DEFAULT_POSTER_THUMB,
'bif_thumb': '', 'bif_thumb': '',
'grandparent_title': 'Plex Media',
'parent_title': 'Plex Media',
'title': 'Plex Media', 'title': 'Plex Media',
'parent_title': 'Plex Media',
'grandparent_title': 'Plex Media',
'original_title': 'Plex Media',
'rating_key': '', 'rating_key': '',
'parent_rating_key': '', 'parent_rating_key': '',
'grandparent_rating_key': '', 'grandparent_rating_key': '',

View File

@@ -521,7 +521,8 @@ class Users(object):
if str(user_id).isdigit(): if str(user_id).isdigit():
query = 'SELECT session_history.id, session_history.media_type, ' \ query = 'SELECT session_history.id, session_history.media_type, ' \
'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \ 'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \
'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \ 'title, parent_title, grandparent_title, original_title, ' \
'thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
'year, started, user ' \ 'year, started, user ' \
'FROM session_history_metadata ' \ 'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
@@ -552,6 +553,7 @@ class Users(object):
'title': row['title'], 'title': row['title'],
'parent_title': row['parent_title'], 'parent_title': row['parent_title'],
'grandparent_title': row['grandparent_title'], 'grandparent_title': row['grandparent_title'],
'original_title': row['original_title'],
'thumb': thumb, 'thumb': thumb,
'media_index': row['media_index'], 'media_index': row['media_index'],
'parent_media_index': row['parent_media_index'], 'parent_media_index': row['parent_media_index'],

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.1.7-beta" PLEXPY_RELEASE_VERSION = "v2.1.13"

View File

@@ -18,6 +18,7 @@ import json
import os import os
import shutil import shutil
import threading import threading
import urllib
import cherrypy import cherrypy
from cherrypy.lib.static import serve_file, serve_download from cherrypy.lib.static import serve_file, serve_download
@@ -56,7 +57,7 @@ import web_socket
from plexpy.api2 import API2 from plexpy.api2 import API2
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
from plexpy.webauth import AuthController, requireAuth, member_of, name_is from plexpy.webauth import AuthController, requireAuth, member_of
def serve_template(templatename, **kwargs): def serve_template(templatename, **kwargs):
@@ -247,23 +248,23 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @addtoapi()
def terminate_session(self, session_id=None, message=None, **kwargs): def terminate_session(self, session_key=None, session_id=None, message=None, **kwargs):
""" Add a new notification agent. """ Stop a streaming session.
``` ```
Required parameters: Required parameters:
session_id (str): The id of the session to terminate session_key (int): The session key of the session to terminate, OR
message (str): A custom message to send to the client session_id (str): The session id of the session to terminate
Optional parameters: Optional parameters:
None message (str): A custom message to send to the client
Returns: Returns:
None None
``` ```
""" """
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.terminate_session(session_id=session_id, message=message) result = pms_connect.terminate_session(session_key=session_key, session_id=session_id, message=message)
if result: if result:
return {'result': 'success', 'message': 'Session terminated.'} return {'result': 'success', 'message': 'Session terminated.'}
@@ -273,8 +274,21 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def return_sessions_url(self, **kwargs): def return_plex_xml_url(self, endpoint='', plextv=False, **kwargs):
return plexpy.CONFIG.PMS_URL + '/status/sessions?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN kwargs['X-Plex-Token'] = plexpy.CONFIG.PMS_TOKEN
if plextv:
base_url = 'https://plex.tv'
else:
if plexpy.CONFIG.PMS_URL_OVERRIDE:
base_url = plexpy.CONFIG.PMS_URL_OVERRIDE
else:
base_url = plexpy.CONFIG.PMS_URL
if '{machine_id}' in endpoint:
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
return base_url + endpoint + '?' + urllib.urlencode(kwargs)
@cherrypy.expose @cherrypy.expose
@requireAuth() @requireAuth()
@@ -1614,6 +1628,7 @@ class WebInterface(object):
"full_title": "Game of Thrones - The Red Woman", "full_title": "Game of Thrones - The Red Woman",
"grandparent_rating_key": 351, "grandparent_rating_key": 351,
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
"original_title": "",
"group_count": 1, "group_count": 1,
"group_ids": "1124", "group_ids": "1124",
"id": 1124, "id": 1124,
@@ -1745,6 +1760,7 @@ class WebInterface(object):
"optimized_version": "", "optimized_version": "",
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"original_title": "",
"pre_tautulli": "", "pre_tautulli": "",
"quality_profile": "1.5 Mbps 480p", "quality_profile": "1.5 Mbps 480p",
"stream_audio_bitrate": 203, "stream_audio_bitrate": 203,
@@ -2826,6 +2842,8 @@ class WebInterface(object):
"show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS, "show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS,
"newsletter_dir": plexpy.CONFIG.NEWSLETTER_DIR, "newsletter_dir": plexpy.CONFIG.NEWSLETTER_DIR,
"newsletter_self_hosted": checked(plexpy.CONFIG.NEWSLETTER_SELF_HOSTED), "newsletter_self_hosted": checked(plexpy.CONFIG.NEWSLETTER_SELF_HOSTED),
"newsletter_auth": plexpy.CONFIG.NEWSLETTER_AUTH,
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES), "newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR "newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
} }
@@ -3220,7 +3238,7 @@ class WebInterface(object):
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @addtoapi()
def set_notifier_config(self, notifier_id=None, agent_id=None, **kwargs): def set_notifier_config(self, notifier_id=None, agent_id=None, **kwargs):
""" Configure an exisitng notificaiton agent. """ Configure an existing notification agent.
``` ```
Required parameters: Required parameters:
@@ -3339,10 +3357,10 @@ class WebInterface(object):
return {'result': 'success', 'message': 'Notification queued.'} return {'result': 'success', 'message': 'Notification queued.'}
else: else:
logger.debug(u"Unable to send %snotification, invalid notifier_id %s." % (test, notifier_id)) logger.debug(u"Unable to send %snotification, invalid notifier_id %s." % (test, notifier_id))
return {'result': 'success', 'message': 'Invalid notifier id %s.' % notifier_id} return {'result': 'error', 'message': 'Invalid notifier id %s.' % notifier_id}
else: else:
logger.debug(u"Unable to send %snotification, no notifier_id received." % test) logger.debug(u"Unable to send %snotification, no notifier_id received." % test)
return {'result': 'success', 'message': 'No notifier id received.'} return {'result': 'error', 'message': 'No notifier id received.'}
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@@ -3479,7 +3497,7 @@ class WebInterface(object):
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @addtoapi()
def set_mobile_device_config(self, mobile_device_id=None, **kwargs): def set_mobile_device_config(self, mobile_device_id=None, **kwargs):
""" Configure an exisitng notificaiton agent. """ Configure an existing notification agent.
``` ```
Required parameters: Required parameters:
@@ -4624,6 +4642,7 @@ class WebInterface(object):
} }
], ],
"media_type": "episode", "media_type": "episode",
"original_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
@@ -4682,6 +4701,7 @@ class WebInterface(object):
"library_name": "", "library_name": "",
"media_index": "1", "media_index": "1",
"media_type": "episode", "media_type": "episode",
"original_title": "",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -4953,6 +4973,7 @@ class WebInterface(object):
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"original_title": "",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210", "parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -5676,7 +5697,7 @@ class WebInterface(object):
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @addtoapi()
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs): def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
""" Configure an exisitng newsletter agent. """ Configure an existing newsletter agent.
``` ```
Required parameters: Required parameters:
@@ -5741,6 +5762,27 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def newsletter(self, *args, **kwargs): def newsletter(self, *args, **kwargs):
request_uri = cherrypy.request.wsgi_environ['REQUEST_URI']
if plexpy.CONFIG.NEWSLETTER_AUTH == 2:
redirect_uri = request_uri.replace('/newsletter', '/newsletter_auth')
raise cherrypy.HTTPRedirect(redirect_uri)
elif plexpy.CONFIG.NEWSLETTER_AUTH == 1 and plexpy.CONFIG.NEWSLETTER_PASSWORD:
if len(args) >= 2 and args[0] == 'image':
return self.newsletter_auth(*args, **kwargs)
elif kwargs.pop('key', None) == plexpy.CONFIG.NEWSLETTER_PASSWORD:
return self.newsletter_auth(*args, **kwargs)
else:
return serve_template(templatename="newsletter_auth.html",
title="Newsletter Login",
uri=request_uri)
else:
return self.newsletter_auth(*args, **kwargs)
@cherrypy.expose
@requireAuth()
def newsletter_auth(self, *args, **kwargs):
if args: if args:
# Keep this for backwards compatibility for images through /newsletter/image # Keep this for backwards compatibility for images through /newsletter/image
if len(args) >= 2 and args[0] == 'image': if len(args) >= 2 and args[0] == 'image':
@@ -5803,3 +5845,8 @@ class WebInterface(object):
logger.error(u"Failed to retrieve newsletter: Missing newsletter_id parameter.") logger.error(u"Failed to retrieve newsletter: Missing newsletter_id parameter.")
return "Failed to retrieve newsletter: missing newsletter_id parameter" return "Failed to retrieve newsletter: missing newsletter_id parameter"
@cherrypy.expose
@requireAuth()
def support(self, query='', **kwargs):
return serve_template(templatename="support.html", title="Support")