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

View File

@@ -1,5 +1,27 @@
# 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)
* 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.
### 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
#### 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.
@@ -29,13 +29,10 @@ Although Tautulli did not adapt a code convention in the past, we try to follow
#### Documentation
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
#### Compatibility
HTML5 compatible browsers are targetted. There is no specific mobile version of Tautulli yet.
HTML5 compatible browsers are targeted.
#### Conventions
* 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 > span.toggle-left {
float: left;
color: #444;
padding-right: 8px;
}
@@ -2945,16 +2946,6 @@ a .home-platforms-list-cover-face:hover
.stacked-configs > li > span > span.active {
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 > span.toggle-left {
color: #999;
@@ -4102,3 +4093,6 @@ a[data-tab-destination] {
margin: 0 auto 50px auto;
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="${href}" title="${data['title']}">${data['title']}</a>
% 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':
<span title="${data['parent_title']}">${data['parent_title']}</span>
% 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)');
$('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
.attr('title', s.grandparent_title)
.text(s.grandparent_title);
.attr('title', s.original_title || s.grandparent_title)
.text(s.original_title || s.grandparent_title);
}
// Update cover if album changed
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
@@ -406,7 +406,11 @@
.text(s.parent_title);
}
// 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)
.attr('href', 'info?rating_key=' + s.rating_key)
.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>
<h2>${data['title']}</h2>
% 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>
<h3 class="hidden-xs">T${data['media_index']}</h3>
% endif
@@ -371,7 +371,11 @@ DOCUMENTATION :: END
<div class="col-md-12">
<div class="table-card-header">
<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>
% endif
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
@@ -502,7 +506,7 @@ DOCUMENTATION :: END
% elif data['media_type'] == 'album':
${data['parent_title']}<br />${data['title']}
% 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
</strong>
</p>

View File

@@ -122,16 +122,24 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'track':
% if loop.index % 2 == 0:
<div class="item-children-list-item-even">
<span class="item-children-list-item-index">${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span>
<span class="item-children-list-item-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>
% 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}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span>
</div>
% else:
<div class="item-children-list-item-odd">
<span class="item-children-list-item-index">${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a></span>
<span class="item-children-list-item-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>
% 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}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</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>
% endif
<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['parent_title']}" class="text-muted">${child['parent_title']}</h3>
</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();
config.css('overflow', 'hidden');
if ($(elem).is(":checked")) {
@@ -36,7 +38,7 @@ function showMsg(msg, loader, timeout, ms, error) {
var message = $("<div class='msg'>" + msg + "</div>");
if (loader) {
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
feedback.css("padding", "14px 10px")
feedback.css("padding", "14px 10px");
}
if (error) {
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').one('click', '#confirm-button', function () {
if (loader_msg) {
showMsg(loader_msg, true, false)
showMsg(loader_msg, true, false);
}
$.ajax({
url: url,
@@ -71,9 +73,9 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
var result = $.parseJSON(xhr.responseText);
var msg = result.message;
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 {
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") {
callback(result);
@@ -85,8 +87,8 @@ function confirmAjaxCall(url, msg, data, loader_msg, callback) {
function doAjaxCall(url, elem, reload, form, showMsg, callback) {
// Set Message
feedback = (showMsg) ? $("#ajaxMsg") : $();
update = $("#updatebar");
var feedback = (showMsg) ? $("#ajaxMsg") : $();
var update = $("#updatebar");
if (update.is(":visible")) {
var height = update.height() + 35;
feedback.css("bottom", height + "px");
@@ -96,8 +98,9 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
feedback.fadeIn();
// Get Form data
var formID = "#" + url;
if (form == true) {
var dataString = $(formID).serialize();
var dataString;
if (form === true) {
dataString = $(formID).serialize();
}
// Loader Image
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');
if (typeof dataSucces === "undefined") {
// Standard Message when variable is not set
var dataSucces = "Success!";
dataSucces = "Success!";
}
// Data Errror Message
var dataError = $(elem).data('error');
if (typeof dataError === "undefined") {
// 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
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 ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ||
$('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) {
feedback.addClass('error')
feedback.addClass('error');
$(feedback).prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(function () {
@@ -128,7 +131,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
feedback.fadeOut(function () {
feedback.removeClass('error');
});
})
});
$(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
}, 2000);
return false;
@@ -144,33 +147,33 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
feedback.prepend(loader);
},
error: function (jqXHR, textStatus, errorThrown) {
feedback.addClass('error')
feedback.addClass('error');
feedback.prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(function () {
$(this).remove();
feedback.fadeOut(function () {
feedback.removeClass('error')
feedback.removeClass('error');
});
});
})
}, 2000);
},
success: function (data, jqXHR) {
feedback.prepend(succesMsg);
feedback.addClass('success')
feedback.addClass('success');
setTimeout(function (e) {
succesMsg.fadeOut(function () {
$(this).remove();
feedback.fadeOut(function () {
feedback.removeClass('success');
});
if (reload == true) refreshSubmenu();
if (reload == "table") {
if (reload === true) refreshSubmenu();
if (reload === "table") {
refreshTable();
}
if (reload == "tabs") refreshTab();
if (reload == "page") location.reload();
if (reload == "submenu&table") {
if (reload === "tabs") refreshTab();
if (reload === "page") location.reload();
if (reload === "submenu&table") {
refreshSubmenu();
refreshTable();
}
@@ -179,7 +182,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
$(formID + " select").children('option[disabled=disabled]').attr(
'selected', 'selected');
}
})
});
}, 2000);
},
complete: function (jqXHR, textStatus) {
@@ -215,19 +218,20 @@ function isPrivateIP(ip_address) {
$.cachedScript('js/ipaddr.min.js').done(function () {
if (ipaddr.isValid(ip_address)) {
var addr = ipaddr.process(ip_address)
var addr = ipaddr.process(ip_address);
var rangeList = [];
if (addr.kind() === 'ipv4') {
var rangeList = [
rangeList = [
ipaddr.parseCIDR('127.0.0.0/8'),
ipaddr.parseCIDR('10.0.0.0/8'),
ipaddr.parseCIDR('172.16.0.0/12'),
ipaddr.parseCIDR('192.168.0.0/16')
]
];
} else {
var rangeList = [
rangeList = [
ipaddr.parseCIDR('fd00::/8')
]
];
}
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
@@ -238,12 +242,13 @@ function isPrivateIP(ip_address) {
} else {
defer.resolve('n/a');
}
})
});
return defer.promise();
}
function humanTime(seconds) {
var text;
if (seconds >= 86400) {
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>' +
@@ -265,6 +270,7 @@ function humanTime(seconds) {
}
function humanTimeClean(seconds) {
var text;
if (seconds >= 86400) {
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + 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++) {
var c = ca[i];
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 "";
}
@@ -354,24 +360,24 @@ var Accordion = function (el, multiple) {
links.on('click', {
el: this.el,
multiple: this.multiple
}, this.dropdown)
}
}, this.dropdown);
};
Accordion.prototype.dropdown = function (e) {
var $el = e.data.el;
$this = $(this),
$this = $(this);
$next = $this.next();
$next.slideToggle();
$this.parent().toggleClass('open');
if (!e.data.multiple) {
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
};
}
}
};
function clearSearchButton(tableName, table) {
$('#' + tableName + '_filter').find('input[type=search]').wrap(
'<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-' +
tableName + '"><i class="fa fa-remove"></i></button></span>')
tableName + '"><i class="fa fa-remove"></i></button></span>');
$('#clear-search-' + tableName).click(function () {
table.search('').draw();
});
@@ -401,7 +407,6 @@ $('*').on('click', '.refresh_pms_image', function (e) {
} else {
if (pms_proxy_url.indexOf('refresh=true') > -1) {
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 + '&refresh=true)');
} else {
@@ -416,8 +421,7 @@ function humanFileSize(bytes, si) {
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
var units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
var u = -1;
do {
@@ -436,10 +440,10 @@ function forceMinMax(elem) {
if (isNaN(val)) {
elem.val(default_val);
}
else if (min != undefined && val < min) {
else if (min !== undefined && val < min) {
elem.val(min);
}
else if (max != undefined && val > max) {
else if (max !== undefined && val > max) {
elem.val(max);
}
else {
@@ -453,4 +457,4 @@ function capitalizeFirstLetter(string) {
$.fn.slideToggleBool = function(bool, options) {
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
}
};

View File

@@ -11,12 +11,11 @@ DOCUMENTATION :: END
<ul class="stacked-configs list-unstyled">
% 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 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-mobile"></i></span>
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></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']}">
% if device['last_seen']:
<script>
@@ -26,14 +25,13 @@ DOCUMENTATION :: END
never
% endif
</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>
</li>
% 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 class="toggle-left"><i class="fa fa-lg fa-mobile"></i></span> Register a new device
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span>
<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-fw fa-plus"></i></span>
</span>
</li>
</ul>

View File

@@ -12,15 +12,15 @@ DOCUMENTATION :: END
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %>
<ul class="stacked-configs list-unstyled">
% 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 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']:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
% else:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']})</span>
% 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']}">
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
@@ -32,10 +32,10 @@ DOCUMENTATION :: END
</span>
</li>
% 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 class="toggle-left"><i class="fa fa-lg 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-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-fw fa-plus"></i></span>
</span>
</li>
</ul>

View File

@@ -1,7 +1,8 @@
% if notifier:
<%!
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()
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':
<div class="checkbox">
<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>
<p class="help-block">${item['description'] | n}</p>
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
@@ -146,7 +147,7 @@
% for action in available_notification_actions:
<div class="checkbox">
<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>
<p class="help-block">${action['description'] | n}</p>
<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">
% 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 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']:
${notifier['agent_label']} &nbsp;<span class="friendly_name">(${notifier['id']} - ${notifier['friendly_name']})</span>
% else:
${notifier['agent_label']} &nbsp;<span class="friendly_name">(${notifier['id']})</span>
% 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>
</li>
% 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 class="toggle-left"><i class="fa fa-lg 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-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-fw fa-plus"></i></span>
</span>
</li>
</ul>

View File

@@ -650,7 +650,7 @@
</div>
<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="col-md-9" id="selectize-pms-ip-container">
<div class="input-group">
@@ -1423,7 +1423,7 @@
<div class="col-md-12">
<ul class="stacked-configs list-unstyled">
% 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>
</li>
% endfor
@@ -1451,7 +1451,7 @@
<div class="col-md-12">
<ul class="stacked-configs list-unstyled">
% 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>
</li>
% endfor
@@ -1793,6 +1793,7 @@
}
function loadNotifierConfig(notifier_id) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({
url: 'get_notifier_config_modal',
data: { notifier_id: notifier_id },
@@ -1800,6 +1801,7 @@
async: true,
complete: function (xhr, status) {
$("#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) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({
url: 'get_newsletter_config_modal',
data: { newsletter_id: newsletter_id },
@@ -1823,6 +1826,7 @@
async: true,
complete: function (xhr, status) {
$("#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) {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Loading Configuration', false);
$.ajax({
url: 'get_mobile_device_config_modal',
data: { mobile_device_id: mobile_device_id },
@@ -1846,6 +1851,7 @@
async: true,
complete: function (xhr, status) {
$("#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 msg = result.message;
$('#add-notifier-modal').modal('hide');
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
loadNotifierConfig(result.notifier_id);
} else {
@@ -2613,7 +2619,7 @@ $(document).ready(function() {
var result = $.parseJSON(xhr.responseText);
var msg = result.message;
$('#add-newsletter-modal').modal('hide');
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
loadNewsletterConfig(result.newsletter_id);
} else {

View File

@@ -46,8 +46,10 @@ DOCUMENTATION :: END
<div class="modal-header">
<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">
% 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>
% elif data['media_type'] == 'track':
Stream Info: <strong>${data['original_title'] or data['grandparent_title']} - ${data['title']} (${user})</strong>
% else:
Stream Info: <strong>${data['title']} (${user})</strong>
% endif

View File

@@ -108,8 +108,8 @@ DOCUMENTATION :: END
</div>
</a>
<div class="dashboard-recent-media-metacontainer">
<h3 title="${item['grandparent_title']}">
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
<h3 title="${item['original_title'] or item['grandparent_title']}">
<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 class="text-muted" title="${item['title']}">
<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);">
<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;">
<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>
<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;">
@@ -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);">
<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;">
<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>
<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;">
@@ -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);">
<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;">
<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>
<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;">
@@ -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;">
<em>${album['parent_title']} &middot; ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
</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;">
${album['summary'][:200] + (album['summary'][200:] and '...')}
</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']});">
<tr>
<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>
<td>
<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']});">
<tr>
<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>
<td>
<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']});">
<tr>
<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>
<td>
<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">
<em>${album['parent_title']} &middot; ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}</em>
</p>
% if artist['title'].lower() != 'various artists':
% if album['parent_title'].lower() != 'various artists':
<p>
${album['summary'][:200] + (album['summary'][200:] and '...')}
</p>

View File

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

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python
import errno
import os
import re
import tempfile
from hashlib import md5
@@ -47,7 +46,7 @@ class _FileCache(object):
path = self._GetPath(key)
if not path.startswith(self._root_directory):
raise _FileCacheError('%s does not appear to live under %s' %
(path, self._root_directory ))
(path, self._root_directory))
if os.path.exists(path):
os.remove(path)
@@ -101,61 +100,3 @@ class _FileCache(object):
def _GetPrefix(self, hashed_key):
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):
'''Returns the first argument used to construct this error.'''
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):
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):
""" Returns the TwitterModel as a JSON string based on key/value
pairs returned from the AsDict() method. """
@@ -78,11 +85,14 @@ class TwitterModel(object):
"""
json_data = data.copy()
if kwargs:
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):
@@ -93,11 +103,14 @@ class Media(TwitterModel):
self.param_defaults = {
'display_url': None,
'expanded_url': None,
'ext_alt_text': None,
'id': None,
'media_url': None,
'media_url_https': None,
'sizes': None,
'type': None,
'url': None,
'video_info': None,
}
for (param, default) in self.param_defaults.items():
@@ -172,8 +185,10 @@ class DirectMessage(TwitterModel):
self.param_defaults = {
'created_at': None,
'id': None,
'recipient': None,
'recipient_id': None,
'recipient_screen_name': None,
'sender': None,
'sender_id': None,
'sender_screen_name': None,
'text': None,
@@ -181,6 +196,10 @@ class DirectMessage(TwitterModel):
for (param, default) in self.param_defaults.items():
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):
if self.text and len(self.text) > 140:
@@ -206,7 +225,7 @@ class Trend(TwitterModel):
'query': None,
'timestamp': None,
'url': None,
'volume': None,
'tweet_volume': None,
}
for (param, default) in self.param_defaults.items():
@@ -218,6 +237,10 @@ class Trend(TwitterModel):
self.timestamp,
self.url)
@property
def volume(self):
return self.tweet_volume
class Hashtag(TwitterModel):
@@ -259,7 +282,7 @@ class UserStatus(TwitterModel):
""" A class representing the UserStatus structure. This is an abbreviated
form of the twitter.User object. """
connections = {'following': False,
_connections = {'following': False,
'followed_by': False,
'following_received': False,
'following_requested': False,
@@ -284,10 +307,19 @@ class UserStatus(TwitterModel):
setattr(self, param, kwargs.get(param, default))
if 'connections' in kwargs:
for param in self.connections:
for param in self._connections:
if param in kwargs['connections']:
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):
connections = [param for param in self.connections if getattr(self, param)]
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
@@ -307,11 +339,14 @@ class User(TwitterModel):
'default_profile': None,
'default_profile_image': None,
'description': None,
'email': None,
'favourites_count': None,
'followers_count': None,
'following': None,
'friends_count': None,
'geo_enabled': None,
'id': None,
'id_str': None,
'lang': None,
'listed_count': None,
'location': None,
@@ -319,12 +354,16 @@ class User(TwitterModel):
'notifications': None,
'profile_background_color': None,
'profile_background_image_url': None,
'profile_background_image_url_https': None,
'profile_background_tile': None,
'profile_banner_url': None,
'profile_image_url': None,
'profile_image_url_https': None,
'profile_link_color': None,
'profile_sidebar_border_color': None,
'profile_sidebar_fill_color': None,
'profile_text_color': None,
'profile_use_background_image': None,
'protected': None,
'screen_name': None,
'status': None,
@@ -333,6 +372,8 @@ class User(TwitterModel):
'url': None,
'utc_offset': None,
'verified': None,
'withheld_in_countries': None,
'withheld_scope': None,
}
for (param, default) in self.param_defaults.items():
@@ -365,6 +406,7 @@ class Status(TwitterModel):
'current_user_retweet': None,
'favorite_count': None,
'favorited': None,
'full_text': None,
'geo': None,
'hashtags': None,
'id': None,
@@ -377,6 +419,9 @@ class Status(TwitterModel):
'media': None,
'place': None,
'possibly_sensitive': None,
'quoted_status': None,
'quoted_status_id': None,
'quoted_status_id_str': None,
'retweet_count': None,
'retweeted': None,
'retweeted_status': None,
@@ -395,6 +440,11 @@ class Status(TwitterModel):
for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default))
if kwargs.get('full_text', None):
self.tweet_mode = 'extended'
else:
self.tweet_mode = 'compatibility'
@property
def created_at_in_seconds(self):
""" 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
the ID of status, username and datetime.
"""
if self.tweet_mode == 'extended':
text = self.full_text
else:
text = self.text
if self.user:
return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format(
self.id,
self.user.screen_name,
self.created_at,
self.text)
text)
else:
return u"Status(ID={0}, Created={1}, Text={2!r})".format(
self.id,
self.created_at,
self.text)
text)
@classmethod
def NewFromJsonDict(cls, data, **kwargs):
@@ -439,17 +493,25 @@ class Status(TwitterModel):
current_user_retweet = None
hashtags = None
media = None
quoted_status = None
retweeted_status = None
urls = None
user = 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:
user = User.NewFromJsonDict(data['user'])
if 'retweeted_status' in data:
retweeted_status = Status.NewFromJsonDict(data['retweeted_status'])
if 'current_user_retweet' in data:
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 'urls' in data['entities']:
@@ -470,6 +532,7 @@ class Status(TwitterModel):
current_user_retweet=current_user_retweet,
hashtags=hashtags,
media=media,
quoted_status=quoted_status,
retweeted_status=retweeted_status,
urls=urls,
user=user,

View File

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

View File

@@ -97,6 +97,7 @@ class RateLimit(object):
and a dictionary of limit, remaining, and reset will be returned.
"""
self.__dict__['resources'] = {}
self.__dict__.update(kwargs)
@staticmethod
@@ -117,10 +118,12 @@ class RateLimit(object):
for non_std_endpoint in NON_STANDARD_ENDPOINTS:
if re.match(non_std_endpoint.regex, resource):
return non_std_endpoint.resource
else:
return resource
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
dictionary. This is to deal with new endpoints being added to
the API, but not necessarily to the information returned by
@@ -146,13 +149,18 @@ class RateLimit(object):
"""
endpoint = self.url_to_resource(url)
resource_family = endpoint.split('/')[1]
self.__dict__['resources'].update(
{resource_family: {
endpoint: {
"limit": limit,
"remaining": remaining,
"reset": reset
}}})
new_endpoint = {endpoint: {
"limit": enf_type('limit', int, limit),
"remaining": enf_type('remaining', int, remaining),
"reset": enf_type('reset', int, 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):
""" Gets a EndpointRateLimit object for the given url.
@@ -181,35 +189,3 @@ class RateLimit(object):
return EndpointRateLimit(family_rates['limit'],
family_rates['remaining'],
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
from __future__ import unicode_literals
import mimetypes
import os
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
from tempfile import NamedTemporaryFile
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 = [
"ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar",
@@ -138,7 +158,14 @@ TLDS = [
"淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团",
"飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "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):
@@ -153,12 +180,19 @@ def calc_expected_status_length(status, short_url_length=23):
Expected length of the status message as an integer.
"""
replaced_chars = 0
status_length = len(status)
match = re.findall(URL_REGEXP, status)
if len(match) >= 1:
replaced_chars = len(''.join(match))
status_length = status_length - replaced_chars + (short_url_length * len(match))
status_length = 0
if isinstance(status, bytes):
status = unicode(status)
for word in re.split(r'\s', status):
if is_url(word):
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
@@ -171,16 +205,14 @@ def is_url(text):
Returns:
Boolean of whether the text should be treated as a URL or not.
"""
if re.findall(URL_REGEXP, text):
return True
else:
return False
return bool(re.findall(URL_REGEXP, text))
def http_to_file(http):
data_file = NamedTemporaryFile()
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
@@ -200,7 +232,8 @@ def parse_media_file(passed_media):
'image/gif',
'image/bmp',
'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,
# 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 passed_media.startswith('http'):
data_file = http_to_file(passed_media)
filename = os.path.basename(passed_media)
filename = os.path.basename(urlparse(passed_media).path)
else:
data_file = open(os.path.realpath(passed_media), 'rb')
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,
# create the standard reference to media_file (i.e., rename it to fp).
else:
if passed_media.mode != 'rb':
raise TwitterError({'message': 'File mode must be "rb".'})
if passed_media.mode not in ['rb', 'rb+', 'w+b']:
raise TwitterError('File mode must be "rb" or "rb+"')
filename = os.path.basename(passed_media.name)
data_file = passed_media
@@ -226,10 +259,11 @@ def parse_media_file(passed_media):
try:
data_file.seek(0)
except:
except Exception as e:
pass
media_type = mimetypes.guess_type(os.path.basename(filename))[0]
if media_type is not None:
if media_type in img_formats and file_size > 5 * 1048576:
raise TwitterError({'message': 'Images must be less than 5MB.'})
elif media_type in video_formats and file_size > 15 * 1048576:
@@ -263,3 +297,18 @@ def enf_type(field, _type, val):
raise TwitterError({
'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, '
'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, '
'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, '
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
'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, '
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title 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, '
'write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)'
)
@@ -580,8 +582,9 @@ def dbcheck():
c_db.execute(
'CREATE TABLE IF NOT EXISTS session_history_metadata (id INTEGER PRIMARY KEY, '
'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, '
'parent_media_index INTEGER, section_id INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, '
'title TEXT, parent_title TEXT, grandparent_title TEXT, original_title TEXT, full_title 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, '
'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, '
@@ -1064,6 +1067,27 @@ def dbcheck():
'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
try:
c_db.execute('SELECT reference_id FROM session_history')
@@ -1150,6 +1174,15 @@ def dbcheck():
'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
try:
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())
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 db_session:
@@ -235,10 +239,11 @@ class ActivityHandler(object):
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
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
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
if this_state == 'playing':
# Update the session in our temp session table

View File

@@ -50,6 +50,7 @@ class ActivityProcessor(object):
'title': session.get('title', ''),
'parent_title': session.get('parent_title', ''),
'grandparent_title': session.get('grandparent_title', ''),
'original_title': session.get('original_title', ''),
'full_title': session.get('full_title', ''),
'media_index': session.get('media_index', ''),
'parent_media_index': session.get('parent_media_index', ''),
@@ -60,6 +61,7 @@ class ActivityProcessor(object):
'friendly_name': session.get('friendly_name', ''),
'ip_address': session.get('ip_address', ''),
'player': session.get('player', ''),
'product': session.get('product', ''),
'platform': session.get('platform', ''),
'parent_rating_key': session.get('parent_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_subtitle_decision': session.get('stream_subtitle_decision', ''),
'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),
'stopped': int(time.time())
}
@@ -396,6 +400,7 @@ class ActivityProcessor(object):
'title': session['title'],
'parent_title': session['parent_title'],
'grandparent_title': session['grandparent_title'],
'original_title': session['original_title'],
'full_title': session['full_title'],
'media_index': metadata['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 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': '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 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'},
@@ -402,6 +403,7 @@ NOTIFICATION_PARAMETERS = [
{'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': '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 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'},

View File

@@ -86,6 +86,7 @@ class DataFactory(object):
'session_history_metadata.title',
'session_history_metadata.parent_title',
'session_history_metadata.grandparent_title',
'session_history_metadata.original_title',
'session_history_metadata.year',
'session_history_metadata.media_index',
'session_history_metadata.parent_media_index',
@@ -132,6 +133,7 @@ class DataFactory(object):
'title',
'parent_title',
'grandparent_title',
'original_title',
'year',
'media_index',
'parent_media_index',
@@ -233,6 +235,7 @@ class DataFactory(object):
'title': item['parent_title'],
'parent_title': item['parent_title'],
'grandparent_title': item['grandparent_title'],
'original_title': item['original_title'],
'year': item['year'],
'media_index': item['media_index'],
'parent_media_index': item['parent_media_index'],
@@ -480,7 +483,8 @@ class DataFactory(object):
elif stat == 'top_music':
top_music = []
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, ' \
'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) - ' \
@@ -492,7 +496,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \
' AND session_history.media_type = "track" ' \
' GROUP BY %s) AS t ' \
'GROUP BY t.grandparent_title ' \
'GROUP BY t.original_title, t.grandparent_title ' \
'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
@@ -501,7 +505,7 @@ class DataFactory(object):
return None
for item in result:
row = {'title': item['grandparent_title'],
row = {'title': item['original_title'] or item['grandparent_title'],
'total_plays': item['total_plays'],
'total_duration': item['total_duration'],
'users_watched': '',
@@ -529,7 +533,8 @@ class DataFactory(object):
elif stat == 'popular_music':
popular_music = []
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, ' \
'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 ' \
@@ -542,7 +547,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \
' AND session_history.media_type = "track" ' \
' 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 ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query)
@@ -551,7 +556,7 @@ class DataFactory(object):
return None
for item in result:
row = {'title': item['grandparent_title'],
row = {'title': item['original_title'] or item['grandparent_title'],
'users_watched': item['users_watched'],
'rating_key': item['grandparent_rating_key'],
'last_play': item['last_watch'],
@@ -888,7 +893,7 @@ class DataFactory(object):
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'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 ' \
'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 ' \
@@ -909,7 +914,7 @@ class DataFactory(object):
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \
'media_type, title, grandparent_title ' \
'media_type, title, grandparent_title, original_title ' \
'FROM sessions ' \
'WHERE session_key = ? %s' % user_cond
result = monitor_db.select(query, args=[session_key])
@@ -979,6 +984,7 @@ class DataFactory(object):
'media_type': item['media_type'],
'title': item['title'],
'grandparent_title': item['grandparent_title'],
'original_title': item['original_title'],
'current_session': 1 if session_key else 0,
'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.grandparent_rating_key, session_history_metadata.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.section_id, session_history_metadata.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'],
'grandparent_rating_key': item['grandparent_rating_key'],
'grandparent_title': item['grandparent_title'],
'original_title': item['original_title'],
'parent_media_index': item['parent_media_index'],
'parent_title': item['parent_title'],
'media_index': item['media_index'],
@@ -1550,8 +1558,11 @@ class DataFactory(object):
if metadata:
# 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'])
elif metadata['media_type'] == 'track':
full_title = '%s - %s' % (metadata['title'],
metadata['original_title'] or metadata['grandparent_title'])
else:
full_title = metadata['title']
@@ -1566,7 +1577,8 @@ class DataFactory(object):
# Update the session_history_metadata table
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 = ?, ' \
'grandparent_thumb = ?, art = ?, media_type = ?, year = ?, originally_available_at = ?, ' \
'added_at = ?, updated_at = ?, last_viewed_at = ?, content_rating = ?, summary = ?, ' \
@@ -1575,7 +1587,8 @@ class DataFactory(object):
'WHERE 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['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],

View File

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

View File

@@ -862,13 +862,13 @@ class Libraries(object):
if str(section_id).isdigit():
query = 'SELECT session_history.id, session_history.media_type, ' \
'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 ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE section_id = ? ' \
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
' ELSE session_history.rating_key END) ' \
'GROUP BY session_history.rating_key ' \
'ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[section_id, limit])
else:
@@ -893,6 +893,7 @@ class Libraries(object):
'title': row['title'],
'parent_title': row['parent_title'],
'grandparent_title': row['grandparent_title'],
'original_title': row['original_title'],
'thumb': thumb,
'media_index': row['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():
agent_id = int(agent_id)
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)
return False
@@ -288,9 +288,9 @@ def serve_template(templatename, **kwargs):
try:
template = _hplookup.get_template(templatename)
return template.render(**kwargs)
return template.render(**kwargs), False
except:
return exceptions.html_error_template().render()
return exceptions.html_error_template().render(), True
def generate_newsletter_uuid():
@@ -376,6 +376,7 @@ class Newsletter(object):
self.newsletter = None
self.is_preview = False
self.template_error = None
def set_config(self, config=None, default=None):
return self._validate_config(config=config, default=default)
@@ -421,7 +422,7 @@ class Newsletter(object):
self.retrieve_data()
newsletter_rendered = serve_template(
newsletter_rendered, self.template_error = serve_template(
templatename=self._TEMPLATE,
uuid=self.uuid,
subject=self.subject_formatted,
@@ -432,14 +433,17 @@ class Newsletter(object):
preview=self.is_preview
)
if self.template_error:
return newsletter_rendered
# Force Tautulli footer
if '<!-- FOOTER MESSAGE - DO NOT REMOVE -->' in newsletter_rendered:
newsletter_rendered = newsletter_rendered.replace(
'<!-- FOOTER MESSAGE - DO NOT REMOVE -->',
'Newsletter generated by <a href="https://tautulli.com" target="_blank" '
'style="text-decoration: underline;color: #fff;font-size: 12px;">Tautulli</a>.'
'style="text-decoration: underline;color: inherit;font-size: inherit;">Tautulli</a>.'
)
return newsletter_rendered
else:
msg = ('<div style="text-align: center;padding-top: 100px;padding-bottom: 100px;">'
'<p style="font-family: \'Open Sans\', Helvetica, Arial, sans-serif;color: #282A2D;'
@@ -449,11 +453,16 @@ class Newsletter(object):
'<a href="https://tautulli.com" target="_blank">Tautulli</a>.<br>Thank you.'
'</p></div>')
newsletter_rendered = re.sub(r'(<body.*?>)', r'\1' + msg, newsletter_rendered)
return newsletter_rendered
def send(self):
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():
logger.warn(u"Tautulli Newsletters :: %s newsletter has no data. Newsletter not sent." % self.NAME)
return False
@@ -783,8 +792,9 @@ class RecentlyAdded(Newsletter):
else:
item['art_hash'] = ''
item['poster_url'] = ''
item['thumb_url'] = ''
item['art_url'] = ''
item['poster_url'] = item['thumb_url'] # Keep for backwards compatibility
elif helpers.get_img_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'],
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=item['art'], rating_key=item['rating_key'], title=item['title'],
@@ -810,6 +820,15 @@ class RecentlyAdded(Newsletter):
item['thumb_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

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'])
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'])
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):
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)
@@ -633,6 +633,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
notify_params['parent_title'])
else:
poster_thumb = ''
poster_key = ''
poster_title = ''
img_service = helpers.get_img_service(include_self=True)
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_profile': notify_params['optimized_version_profile'],
'synced_version': notify_params['synced_version'],
'live': notify_params['live'],
'stream_local': notify_params['local'],
'stream_location': notify_params['location'],
'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,
'album_name': album_name,
'track_name': track_name,
'track_artist': notify_params['original_title'] or notify_params['grandparent_title'],
'season_num': season_num,
'season_num00': season_num00,
'episode_num': episode_num,

View File

@@ -509,7 +509,7 @@ def add_notifier_config(agent_id=None, **kwargs):
'agent_name': agent['name'],
'agent_label': agent['label'],
'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_logic': ''
}
@@ -540,7 +540,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
if str(agent_id).isdigit():
agent_id = int(agent_id)
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)
return False
@@ -570,7 +570,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
'agent_name': agent['name'],
'agent_label': agent['label'],
'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_logic': kwargs.get('custom_conditions_logic', ''),
}
@@ -728,7 +728,7 @@ class PrettyMetadata(object):
elif self.media_type == 'album':
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
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")
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:
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:
logger.error(u"Tautulli Notifiers :: {}".format(err_msg))
@@ -1145,7 +1145,8 @@ class DISCORD(Notifier):
plex_url = pretty_metadata.get_plex_url()
# Build Discord post attachment
attachment = {'title': title
attachment = {'title': title,
'timestamp': helpers.utc_now_iso()
}
if self.config['color']:
@@ -1318,10 +1319,11 @@ class EMAIL(Notifier):
recipients = self.config['to'] + self.config['cc'] + self.config['bcc']
mailserver = None
success = False
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
try:
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
mailserver.ehlo()
if self.config['tls']:
@@ -1332,14 +1334,15 @@ class EMAIL(Notifier):
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
mailserver.sendmail(self.config['from'], recipients, msg.as_string())
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
success = True
except Exception as e:
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
finally:
if mailserver:
mailserver.quit()
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
return success

View File

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

View File

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

View File

@@ -512,6 +512,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(m, 'title'),
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
'media_index': helpers.get_xml_attr(m, 'index'),
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
@@ -661,6 +662,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -920,6 +927,8 @@ class PmsConnect(object):
elif metadata_type == 'track':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
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,
'section_id': section_id,
'library_name': library_name,
@@ -929,6 +938,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -957,8 +967,8 @@ class PmsConnect(object):
'genres': album_details['genres'],
'labels': album_details['labels'],
'collections': album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title')),
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'),
track_artist),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
@@ -972,6 +982,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'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'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'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_title': helpers.get_xml_attr(stream_media_info, 'title'),
'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'),
'bif_thumb': bif_thumb,
'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():
channel_stream = 1
clip_media = session.getElementsByTagName('Media')[0]
clip_part = clip_media.getElementsByTagName('Part')[0]
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels')
metadata_details = {'media_type': media_type,
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
'library_name': helpers.get_xml_attr(session, 'librarySectionTitle'),
@@ -1682,6 +1696,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(session, 'title'),
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(session, 'originalTitle'),
'sort_title': helpers.get_xml_attr(session, 'titleSort'),
'media_index': helpers.get_xml_attr(session, 'index'),
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
@@ -1710,18 +1725,17 @@ class PmsConnect(object):
'genres': [],
'labels': [],
'full_title': helpers.get_xml_attr(session, 'title'),
'container': helpers.get_xml_attr(clip_media, 'container') \
or helpers.get_xml_attr(clip_part, 'container'),
'height': helpers.get_xml_attr(clip_media, 'height'),
'width': helpers.get_xml_attr(clip_media, 'width'),
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(clip_media, 'videoResolution'),
'audio_codec': helpers.get_xml_attr(clip_media, 'audioCodec'),
'container': helpers.get_xml_attr(stream_media_info, 'container') \
or helpers.get_xml_attr(stream_media_parts_info, 'container'),
'height': helpers.get_xml_attr(stream_media_info, 'height'),
'width': helpers.get_xml_attr(stream_media_info, 'width'),
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution'),
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
'audio_channels': audio_channels,
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
'live': int(helpers.get_xml_attr(session, 'live') == '1'),
'extra_type': helpers.get_xml_attr(session, 'extraType'),
'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))
# 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'] = transcode_details['transcode_container']
video_details['stream_video_decision'] = transcode_details['video_decision']
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
stream_details['stream_video_resolution'] = metadata_details['video_resolution']
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
@@ -1994,6 +2007,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(m, 'title'),
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(m, 'originalTitle'),
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
'media_index': helpers.get_xml_attr(m, 'index'),
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
@@ -2311,6 +2325,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
'original_title': helpers.get_xml_attr(item, 'originalTitle'),
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
'media_index': helpers.get_xml_attr(item, 'index'),
'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,
'thumb': common.DEFAULT_POSTER_THUMB,
'bif_thumb': '',
'grandparent_title': 'Plex Media',
'parent_title': 'Plex Media',
'title': 'Plex Media',
'parent_title': 'Plex Media',
'grandparent_title': 'Plex Media',
'original_title': 'Plex Media',
'rating_key': '',
'parent_rating_key': '',
'grandparent_rating_key': '',

View File

@@ -521,7 +521,8 @@ class Users(object):
if str(user_id).isdigit():
query = 'SELECT session_history.id, session_history.media_type, ' \
'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 ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
@@ -552,6 +553,7 @@ class Users(object):
'title': row['title'],
'parent_title': row['parent_title'],
'grandparent_title': row['grandparent_title'],
'original_title': row['original_title'],
'thumb': thumb,
'media_index': row['media_index'],
'parent_media_index': row['parent_media_index'],

View File

@@ -1,2 +1,2 @@
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",
"grandparent_rating_key": 351,
"grandparent_title": "Game of Thrones",
"original_title": "",
"group_count": 1,
"group_ids": "1124",
"id": 1124,
@@ -1745,6 +1746,7 @@ class WebInterface(object):
"optimized_version": "",
"optimized_version_profile": "",
"optimized_version_title": "",
"original_title": "",
"pre_tautulli": "",
"quality_profile": "1.5 Mbps 480p",
"stream_audio_bitrate": 203,
@@ -3222,7 +3224,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
@addtoapi()
def set_notifier_config(self, notifier_id=None, agent_id=None, **kwargs):
""" Configure an exisitng notificaiton agent.
""" Configure an existing notification agent.
```
Required parameters:
@@ -3341,10 +3343,10 @@ class WebInterface(object):
return {'result': 'success', 'message': 'Notification queued.'}
else:
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:
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.tools.json_out()
@@ -3481,7 +3483,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
@addtoapi()
def set_mobile_device_config(self, mobile_device_id=None, **kwargs):
""" Configure an exisitng notificaiton agent.
""" Configure an existing notification agent.
```
Required parameters:
@@ -4626,6 +4628,7 @@ class WebInterface(object):
}
],
"media_type": "episode",
"original_title": "",
"originally_available_at": "2016-04-24",
"parent_media_index": "6",
"parent_rating_key": "153036",
@@ -4684,6 +4687,7 @@ class WebInterface(object):
"library_name": "",
"media_index": "1",
"media_type": "episode",
"original_title": "",
"parent_media_index": "6",
"parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -4955,6 +4959,7 @@ class WebInterface(object):
"optimized_version_profile": "",
"optimized_version_title": "",
"originally_available_at": "2016-04-24",
"original_title": "",
"parent_media_index": "6",
"parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -5678,7 +5683,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
@addtoapi()
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
""" Configure an exisitng newsletter agent.
""" Configure an existing newsletter agent.
```
Required parameters: