Compare commits
82 Commits
v2.1.7-bet
...
v2.1.13
Author | SHA1 | Date | |
---|---|---|---|
![]() |
70b6bd4efd | ||
![]() |
4f4a99d695 | ||
![]() |
f65baa22f1 | ||
![]() |
a1fb081d47 | ||
![]() |
63ac3eec98 | ||
![]() |
73c11c053d | ||
![]() |
85c398ce05 | ||
![]() |
7387cb8322 | ||
![]() |
37841b6e8c | ||
![]() |
af1355e220 | ||
![]() |
37bd005907 | ||
![]() |
311030b58e | ||
![]() |
bdd124327f | ||
![]() |
70e5e698fa | ||
![]() |
33e799955e | ||
![]() |
331be52327 | ||
![]() |
6ece690e23 | ||
![]() |
6b63a1399e | ||
![]() |
5c77cf652b | ||
![]() |
d6f9a82edb | ||
![]() |
a887489666 | ||
![]() |
8c0bcd0059 | ||
![]() |
c18ee81130 | ||
![]() |
44428cc6e5 | ||
![]() |
c19cc858bd | ||
![]() |
668913fd60 | ||
![]() |
50c5407a46 | ||
![]() |
939755d3b7 | ||
![]() |
54f4696713 | ||
![]() |
c85af521fe | ||
![]() |
917d19db85 | ||
![]() |
7292f25eb9 | ||
![]() |
22a2ad4bc7 | ||
![]() |
95e56f5ea5 | ||
![]() |
ed24232a0a | ||
![]() |
15225faee7 | ||
![]() |
041a35a35a | ||
![]() |
6d365c174a | ||
![]() |
b5f2f55972 | ||
![]() |
ac207260c8 | ||
![]() |
e93808381c | ||
![]() |
7acb8f7dc5 | ||
![]() |
ba9f4a1f9e | ||
![]() |
8502c28e25 | ||
![]() |
10add90451 | ||
![]() |
ddb7fa04ca | ||
![]() |
e21a13b7ff | ||
![]() |
1245b4fbd3 | ||
![]() |
94b00c75c2 | ||
![]() |
2edcf26110 | ||
![]() |
a9fdf73e8b | ||
![]() |
4884cee309 | ||
![]() |
b3c7256bcf | ||
![]() |
2c9a7ced13 | ||
![]() |
aa365eb6a3 | ||
![]() |
2366a8811b | ||
![]() |
53aafbd19e | ||
![]() |
d5bffc374c | ||
![]() |
5cd5c36d8c | ||
![]() |
7f9e8f6211 | ||
![]() |
f743a817ba | ||
![]() |
8e4aba7ed4 | ||
![]() |
8c0ef75d4c | ||
![]() |
76c4b3bb71 | ||
![]() |
112b1c7984 | ||
![]() |
c22a2513e3 | ||
![]() |
f336782fc1 | ||
![]() |
c19afa06de | ||
![]() |
e003850d31 | ||
![]() |
23cf790079 | ||
![]() |
e7f930bd0f | ||
![]() |
348707b6b9 | ||
![]() |
7ad78b4536 | ||
![]() |
a408a62234 | ||
![]() |
a1e9e7e87f | ||
![]() |
fa99f6e684 | ||
![]() |
11e9bd2d54 | ||
![]() |
50165af4b7 | ||
![]() |
5dd22c23f2 | ||
![]() |
79b45c1c46 | ||
![]() |
af917c4915 | ||
![]() |
c3238b5a83 |
19
API.md
19
API.md
@@ -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
|
||||||
|
65
CHANGELOG.md
65
CHANGELOG.md
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Tautulli
|
# Tautulli
|
||||||
|
|
||||||
[](https://discord.gg/tQcWEUp)
|
[](https://tautulli.com/discord)
|
||||||
[](https://www.reddit.com/r/Tautulli/)
|
[](https://www.reddit.com/r/Tautulli/)
|
||||||
[](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server)
|
[](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)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
@@ -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':
|
||||||
|
@@ -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>
|
||||||
|
BIN
data/interfaces/default/images/discord-overlay.png
Normal file
BIN
data/interfaces/default/images/discord-overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 260 KiB |
@@ -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>
|
<h3><span id="sessions-xml">Activity</span>
|
||||||
<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>
|
||||||
|
@@ -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 });
|
||||||
|
@@ -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"> ${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"> ${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>
|
||||||
|
@@ -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
5
data/interfaces/default/js/fontawesome-v4-shims.min.js
vendored
Normal file
5
data/interfaces/default/js/fontawesome-v4-shims.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
data/interfaces/default/js/fontawesome-v5.0.13.min.js
vendored
Normal file
5
data/interfaces/default/js/fontawesome-v5.0.13.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -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 + ' ' + date + '</div></a></div>');
|
$(td).html('<div><a href="#"><div style="float: left;">' + expand_history + ' ' + date + '</div></a></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i> ' + date + '</div>');
|
$(td).html('<div style="float: left;"><i class="fa fa-plus-circle fa-fw fa-blank"> </i> ' + date + '</div>');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
|
@@ -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 + ' ' + date + '</div></a></div>');
|
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i> ' + date + '</div>');
|
$(td).html('<div style="float: left;"><i class="fa fa-plus-circle fa-fw fa-blank"></i> ' + date + '</div>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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>
|
||||||
|
@@ -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']} <span class="friendly_name">(${device['id']})</span>
|
${device['friendly_name'] or device['device_name']} <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>
|
||||||
|
43
data/interfaces/default/newsletter_auth.html
Normal file
43
data/interfaces/default/newsletter_auth.html
Normal 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> Enter</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -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']} <span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
|
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
|
||||||
% else:
|
% else:
|
||||||
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']})</span>
|
${newsletter['agent_label']} <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>
|
||||||
|
@@ -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> ${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>
|
||||||
|
${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> ${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>
|
||||||
|
${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">
|
||||||
|
@@ -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']} <span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
|
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
|
||||||
% else:
|
% else:
|
||||||
${notifier['agent_label']} <span class="friendly_name">(${notifier['id']})</span>
|
${notifier['agent_label']} <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>
|
||||||
|
@@ -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> 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> 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> 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>
|
||||||
|
@@ -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">×</span></button>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</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
|
||||||
|
68
data/interfaces/default/support.html
Normal file
68
data/interfaces/default/support.html
Normal 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> 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> 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>
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
@@ -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']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
<em>${album['parent_title']} · ${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>
|
||||||
|
@@ -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']} · ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
|
<em>${album['parent_title']} · ${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>
|
||||||
|
@@ -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'
|
||||||
|
@@ -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)
|
|
||||||
|
2183
lib/twitter/api.py
2183
lib/twitter/api.py
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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'])
|
|
||||||
|
@@ -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])
|
||||||
|
@@ -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')
|
||||||
|
@@ -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
|
||||||
|
@@ -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 = {}
|
||||||
|
@@ -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))
|
||||||
|
@@ -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.'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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 ' \
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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': '',
|
||||||
|
@@ -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'],
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.1.7-beta"
|
PLEXPY_RELEASE_VERSION = "v2.1.13"
|
||||||
|
@@ -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")
|
||||||
|
Reference in New Issue
Block a user