Compare commits

...

28 Commits

Author SHA1 Message Date
JonnyWong16
b5f2f55972 v2.1.10-beta 2018-05-28 17:22:16 -07:00
JonnyWong16
ac207260c8 Do not send newsletter if failed to render template 2018-05-27 23:02:56 -07:00
JonnyWong16
e93808381c Fix track listing layout on info pages 2018-05-27 22:43:56 -07:00
JonnyWong16
7acb8f7dc5 Fix artist summary not showing up on newsletter 2018-05-27 22:35:54 -07:00
JonnyWong16
ba9f4a1f9e Use track artist for music 2018-05-27 22:24:43 -07:00
JonnyWong16
8502c28e25 Fallback poster_key and poster_title for clip notification 2018-05-27 15:39:28 -07:00
JonnyWong16
10add90451 Merge pull request #1295 from samwiseg00/feature-add-timestamp-discord
Add timestamps for rich metadata info on discord
2018-05-27 14:47:53 -07:00
samwiseg00
ddb7fa04ca add timestamps for rich metadata info on discord 2018-05-27 17:44:35 -04:00
JonnyWong16
e21a13b7ff Revert "Hack to check for live tv stopped websocket event"
This reverts commit 1245b4fbd3.
2018-05-27 14:13:24 -07:00
JonnyWong16
1245b4fbd3 Hack to check for live tv stopped websocket event 2018-05-27 14:04:47 -07:00
JonnyWong16
94b00c75c2 Enable notifications for clip media type 2018-05-27 13:41:56 -07:00
JonnyWong16
2edcf26110 Use HTTPS for cloudinary urls 2018-05-27 13:07:18 -07:00
JonnyWong16
a9fdf73e8b Check live tv websocket event using key instead of rating key 2018-05-27 13:00:34 -07:00
JonnyWong16
4884cee309 Fix live tv stream resolution 2018-05-27 10:13:42 -07:00
JonnyWong16
b3c7256bcf Newsletter footer inherit styles 2018-05-26 17:29:21 -07:00
JonnyWong16
2c9a7ced13 Forgot product in session db write 2018-05-26 10:14:54 -07:00
JonnyWong16
aa365eb6a3 Improved checking of live tv session websocket events 2018-05-26 10:14:36 -07:00
JonnyWong16
2366a8811b Catch exception from failed SMTP connection 2018-05-25 12:19:46 -07:00
JonnyWong16
53aafbd19e Fix typo from d5bffc3 2018-05-25 12:18:29 -07:00
JonnyWong16
d5bffc374c Fallback to blank poster/art on newsletter if image hosting is disabled 2018-05-25 08:26:25 -07:00
JonnyWong16
5cd5c36d8c Actually add live notification parameter 2018-05-23 17:17:47 -07:00
JonnyWong16
7f9e8f6211 Clean up script.js 2018-05-23 17:13:20 -07:00
JonnyWong16
f743a817ba Update python-twitter to 3.4.1 2018-05-23 17:12:19 -07:00
JonnyWong16
8e4aba7ed4 v2.1.9 2018-05-21 09:07:12 -07:00
JonnyWong16
8c0ef75d4c Fix typos and some cleanup 2018-05-21 09:07:01 -07:00
JonnyWong16
76c4b3bb71 Add Live to notification parameter 2018-05-21 08:49:35 -07:00
JonnyWong16
112b1c7984 Refactor css pointer class 2018-05-20 17:04:55 -07:00
JonnyWong16
c22a2513e3 Update CONTRIBUTING.md 2018-05-19 09:12:13 -07:00
44 changed files with 1875 additions and 1183 deletions

11
API.md
View File

@@ -434,6 +434,7 @@ Returns:
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"original_title": "",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210", "parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -678,6 +679,7 @@ Returns:
"full_title": "Game of Thrones - The Red Woman", "full_title": "Game of Thrones - The Red Woman",
"grandparent_rating_key": 351, "grandparent_rating_key": 351,
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
"original_title": "",
"group_count": 1, "group_count": 1,
"group_ids": "1124", "group_ids": "1124",
"id": 1124, "id": 1124,
@@ -1172,6 +1174,7 @@ Returns:
} }
], ],
"media_type": "episode", "media_type": "episode",
"original_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
@@ -1779,6 +1782,7 @@ Returns:
"library_name": "", "library_name": "",
"media_index": "1", "media_index": "1",
"media_type": "episode", "media_type": "episode",
"original_title": "",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -1953,6 +1957,7 @@ Returns:
"optimized_version": "", "optimized_version": "",
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"original_title": "",
"pre_tautulli": "", "pre_tautulli": "",
"quality_profile": "1.5 Mbps 480p", "quality_profile": "1.5 Mbps 480p",
"stream_audio_bitrate": 203, "stream_audio_bitrate": 203,
@@ -2545,7 +2550,7 @@ Returns:
### set_mobile_device_config ### set_mobile_device_config
Configure an exisitng notificaiton agent. Configure an existing notification agent.
``` ```
Required parameters: Required parameters:
@@ -2560,7 +2565,7 @@ Returns:
### set_newsletter_config ### set_newsletter_config
Configure an exisitng newsletter agent. Configure an existing newsletter agent.
``` ```
Required parameters: Required parameters:
@@ -2576,7 +2581,7 @@ Returns:
### set_notifier_config ### set_notifier_config
Configure an exisitng notificaiton agent. Configure an existing notification agent.
``` ```
Required parameters: Required parameters:

View File

@@ -1,5 +1,27 @@
# Changelog # Changelog
## 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) ## v2.1.8-beta (2018-05-19)
* Newsletters: * Newsletters:

View File

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

View File

@@ -2935,6 +2935,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 +2946,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;
@@ -4102,3 +4093,6 @@ a[data-tab-destination] {
margin: 0 auto 50px auto; margin: 0 auto 50px auto;
text-align: center; text-align: center;
} }
.pointer {
cursor: pointer;
}

View File

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

View File

@@ -390,8 +390,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 +406,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)

View File

@@ -165,7 +165,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 +371,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 +506,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>

View File

@@ -122,16 +122,24 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'track': % elif data['children_type'] == 'track':
% if loop.index % 2 == 0: % if loop.index % 2 == 0:
<div class="item-children-list-item-even"> <div class="item-children-list-item-even">
<span class="item-children-list-item-index">${child['media_index']}</span> <span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span> <span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
% if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span>
% endif
</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}"> <span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script> <script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span> </span>
</div> </div>
% else: % else:
<div class="item-children-list-item-odd"> <div class="item-children-list-item-odd">
<span class="item-children-list-item-index">${child['media_index']}</span> <span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span> <span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
% if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span>
% endif
</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}"> <span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script> <script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span> </span>

View File

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

View File

@@ -1,4 +1,6 @@
function initConfigCheckbox(elem, toggleElem = null, reverse = false) { function initConfigCheckbox(elem, toggleElem, reverse) {
toggleElem = (toggleElem === undefined) ? null : toggleElem;
reverse = (reverse === undefined) ? false : reverse;
var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next(); var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
config.css('overflow', 'hidden'); config.css('overflow', 'hidden');
if ($(elem).is(":checked")) { if ($(elem).is(":checked")) {
@@ -36,7 +38,7 @@ function showMsg(msg, loader, timeout, ms, error) {
var message = $("<div class='msg'>" + msg + "</div>"); var message = $("<div class='msg'>" + msg + "</div>");
if (loader) { if (loader) {
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>"); message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
feedback.css("padding", "14px 10px") feedback.css("padding", "14px 10px");
} }
if (error) { if (error) {
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,8 +98,9 @@ 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'></i>");
@@ -105,13 +108,13 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
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 Errror 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,7 +182,7 @@ 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) {
@@ -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,4 @@ 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);
} };

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
% if notifier: % if notifier:
<%! <%!
import json import json
from plexpy import helpers, notifiers, users from plexpy import notifiers, users
from plexpy.helpers import checked
available_notification_actions = notifiers.available_notification_actions() available_notification_actions = notifiers.available_notification_actions()
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']] user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
@@ -70,7 +71,7 @@
% elif item['input_type'] == 'checkbox': % elif item['input_type'] == 'checkbox':
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${helpers.checked(item['value'])}> ${item['label']} <input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
</label> </label>
<p class="help-block">${item['description'] | n}</p> <p class="help-block">${item['description'] | n}</p>
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}"> <input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
@@ -146,7 +147,7 @@
% for action in available_notification_actions: % for action in available_notification_actions:
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${helpers.checked(notifier['actions'][action['name']])}> ${action['label']} <input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${checked(notifier['actions'][action['name']])}> ${action['label']}
</label> </label>
<p class="help-block">${action['description'] | n}</p> <p class="help-block">${action['description'] | n}</p>
<input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}"> <input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}">

View File

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

View File

@@ -650,7 +650,7 @@
</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">
@@ -1423,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
@@ -1451,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
@@ -1793,6 +1793,7 @@
} }
function loadNotifierConfig(notifier_id) { function loadNotifierConfig(notifier_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({ $.ajax({
url: 'get_notifier_config_modal', url: 'get_notifier_config_modal',
data: { notifier_id: notifier_id }, data: { notifier_id: notifier_id },
@@ -1800,6 +1801,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);
} }
}); });
} }
@@ -1816,6 +1818,7 @@
} }
function loadNewsletterConfig(newsletter_id) { function loadNewsletterConfig(newsletter_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({ $.ajax({
url: 'get_newsletter_config_modal', url: 'get_newsletter_config_modal',
data: { newsletter_id: newsletter_id }, data: { newsletter_id: newsletter_id },
@@ -1823,6 +1826,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);
} }
}); });
} }
@@ -1839,6 +1843,7 @@
} }
function loadMobileDeviceConfig(mobile_device_id) { function loadMobileDeviceConfig(mobile_device_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({ $.ajax({
url: 'get_mobile_device_config_modal', url: 'get_mobile_device_config_modal',
data: { mobile_device_id: mobile_device_id }, data: { mobile_device_id: mobile_device_id },
@@ -1846,6 +1851,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);
} }
}); });
} }
@@ -2591,7 +2597,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 {
@@ -2613,7 +2619,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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -520,7 +520,8 @@ def dbcheck():
'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 +541,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 +582,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, '
@@ -1064,6 +1067,27 @@ 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 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 +1174,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')

View File

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

View File

@@ -50,6 +50,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 +61,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 +116,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())
} }
@@ -396,6 +400,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'],

View File

@@ -339,6 +339,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'},
@@ -402,6 +403,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'},

View File

@@ -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'],
@@ -1550,8 +1558,11 @@ class DataFactory(object):
if metadata: if metadata:
# Create full_title # Create full_title
if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track': if metadata['media_type'] == 'episode':
full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title']) full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
elif metadata['media_type'] == 'track':
full_title = '%s - %s' % (metadata['title'],
metadata['original_title'] or metadata['grandparent_title'])
else: else:
full_title = metadata['title'] full_title = metadata['title']
@@ -1566,7 +1577,8 @@ class DataFactory(object):
# Update the session_history_metadata table # Update the session_history_metadata table
query = 'UPDATE session_history_metadata SET rating_key = ?, parent_rating_key = ?, ' \ query = 'UPDATE session_history_metadata SET rating_key = ?, parent_rating_key = ?, ' \
'grandparent_rating_key = ?, title = ?, parent_title = ?, grandparent_title = ?, full_title = ?, ' \ 'grandparent_rating_key = ?, title = ?, parent_title = ?, grandparent_title = ?, ' \
'original_title = ?, full_title = ?, ' \
'media_index = ?, parent_media_index = ?, section_id = ?, thumb = ?, parent_thumb = ?, ' \ 'media_index = ?, parent_media_index = ?, section_id = ?, thumb = ?, parent_thumb = ?, ' \
'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \ 'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \
'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \ 'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \
@@ -1575,7 +1587,8 @@ class DataFactory(object):
'WHERE rating_key = ?' 'WHERE rating_key = ?'
args = [metadata['rating_key'], metadata['parent_rating_key'], metadata['grandparent_rating_key'], args = [metadata['rating_key'], metadata['parent_rating_key'], metadata['grandparent_rating_key'],
metadata['title'], metadata['parent_title'], metadata['grandparent_title'], full_title, metadata['title'], metadata['parent_title'], metadata['grandparent_title'],
metadata['original_title'], full_title,
metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'], metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'],
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'], metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'], metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],

View File

@@ -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'):
@@ -837,7 +840,8 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
img_options = {'format': img_format, img_options = {'format': img_format,
'fetch_format': 'auto', 'fetch_format': 'auto',
'quality': 'auto', 'quality': 'auto',
'version': int(time.time())} 'version': int(time.time()),
'secure': True}
if width != 1000: if width != 1000:
img_options['width'] = str(width) img_options['width'] = str(width)

View File

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

View File

@@ -206,7 +206,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
@@ -288,9 +288,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():
@@ -376,6 +376,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)
@@ -421,7 +422,7 @@ class Newsletter(object):
self.retrieve_data() self.retrieve_data()
newsletter_rendered = 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,
@@ -432,14 +433,17 @@ class Newsletter(object):
preview=self.is_preview preview=self.is_preview
) )
if self.template_error:
return newsletter_rendered
# Force Tautulli footer # Force Tautulli footer
if '<!-- FOOTER MESSAGE - DO NOT REMOVE -->' in newsletter_rendered: if '<!-- FOOTER MESSAGE - DO NOT REMOVE -->' in newsletter_rendered:
newsletter_rendered = newsletter_rendered.replace( newsletter_rendered = newsletter_rendered.replace(
'<!-- FOOTER MESSAGE - DO NOT REMOVE -->', '<!-- FOOTER MESSAGE - DO NOT REMOVE -->',
'Newsletter generated by <a href="https://tautulli.com" target="_blank" ' 'Newsletter generated by <a href="https://tautulli.com" target="_blank" '
'style="text-decoration: underline;color: #fff;font-size: 12px;">Tautulli</a>.' 'style="text-decoration: underline;color: inherit;font-size: inherit;">Tautulli</a>.'
) )
return newsletter_rendered
else: else:
msg = ('<div style="text-align: center;padding-top: 100px;padding-bottom: 100px;">' msg = ('<div style="text-align: center;padding-top: 100px;padding-bottom: 100px;">'
'<p style="font-family: \'Open Sans\', Helvetica, Arial, sans-serif;color: #282A2D;' '<p style="font-family: \'Open Sans\', Helvetica, Arial, sans-serif;color: #282A2D;'
@@ -449,11 +453,16 @@ class Newsletter(object):
'<a href="https://tautulli.com" target="_blank">Tautulli</a>.<br>Thank you.' '<a href="https://tautulli.com" target="_blank">Tautulli</a>.<br>Thank you.'
'</p></div>') '</p></div>')
newsletter_rendered = re.sub(r'(<body.*?>)', r'\1' + msg, newsletter_rendered) newsletter_rendered = re.sub(r'(<body.*?>)', r'\1' + msg, newsletter_rendered)
return 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
@@ -783,8 +792,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
@@ -800,7 +810,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'],
@@ -810,6 +820,15 @@ class RecentlyAdded(Newsletter):
item['thumb_hash'] = '' item['thumb_hash'] = ''
item['art_hash'] = '' item['art_hash'] = ''
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
else:
for item in movies + shows + albums:
item['thumb_hash'] = ''
item['art_hash'] = ''
item['thumb_url'] = ''
item['art_url'] = ''
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
self.data['recently_added'] = recently_added self.data['recently_added'] = recently_added

View File

@@ -169,7 +169,7 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id']) user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
return stream_data['machine_id'] not in user_devices return stream_data['machine_id'] not in user_devices
elif stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode': elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration']) progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
if notify_action == 'on_stop': if notify_action == 'on_stop':
@@ -326,7 +326,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs): def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
logger.info(u"Tautulli NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id) logger.info(u"Tautulli NotificationHandler :: Preparing notification for notifier_id %s." % notifier_id)
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id) notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
@@ -633,6 +633,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
notify_params['parent_title']) notify_params['parent_title'])
else: else:
poster_thumb = '' poster_thumb = ''
poster_key = ''
poster_title = ''
img_service = helpers.get_img_service(include_self=True) img_service = helpers.get_img_service(include_self=True)
if img_service not in (None, 'self-hosted'): if img_service not in (None, 'self-hosted'):
@@ -742,6 +744,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 +805,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,

View File

@@ -509,7 +509,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 +540,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 +570,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', ''),
} }
@@ -728,7 +728,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 +807,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 +1145,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']:
@@ -1318,10 +1319,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 +1334,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

View File

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

View File

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

View File

@@ -512,6 +512,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'),
@@ -661,6 +662,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 +710,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 +755,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 +801,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 +845,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 +890,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 +927,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 +938,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 +967,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 +982,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 +1027,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'),
@@ -1060,6 +1072,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 +1118,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'),
@@ -1658,6 +1672,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 +1686,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 +1696,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 +1725,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 +1804,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']
@@ -1994,6 +2007,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 +2325,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'),

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.1.8-beta" PLEXPY_RELEASE_VERSION = "v2.1.10-beta"

View File

@@ -1614,6 +1614,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 +1746,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,
@@ -3222,7 +3224,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:
@@ -3341,10 +3343,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()
@@ -3481,7 +3483,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:
@@ -4626,6 +4628,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",
@@ -4684,6 +4687,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",
@@ -4955,6 +4959,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",
@@ -5678,7 +5683,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: