Compare commits
23 Commits
v2.1.9
...
v2.1.10-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b5f2f55972 | ||
![]() |
ac207260c8 | ||
![]() |
e93808381c | ||
![]() |
7acb8f7dc5 | ||
![]() |
ba9f4a1f9e | ||
![]() |
8502c28e25 | ||
![]() |
10add90451 | ||
![]() |
ddb7fa04ca | ||
![]() |
e21a13b7ff | ||
![]() |
1245b4fbd3 | ||
![]() |
94b00c75c2 | ||
![]() |
2edcf26110 | ||
![]() |
a9fdf73e8b | ||
![]() |
4884cee309 | ||
![]() |
b3c7256bcf | ||
![]() |
2c9a7ced13 | ||
![]() |
aa365eb6a3 | ||
![]() |
2366a8811b | ||
![]() |
53aafbd19e | ||
![]() |
d5bffc374c | ||
![]() |
5cd5c36d8c | ||
![]() |
7f9e8f6211 | ||
![]() |
f743a817ba |
11
API.md
11
API.md
@@ -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:
|
||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,21 @@
|
||||
# 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:
|
||||
|
@@ -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':
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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"> ${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"> ${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>
|
||||
|
@@ -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>
|
||||
|
@@ -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),
|
||||
$next = $this.next();
|
||||
$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);
|
||||
}
|
||||
};
|
@@ -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">×</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
|
||||
|
@@ -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>
|
||||
|
@@ -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']} · ${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>
|
||||
|
@@ -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']} · ${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>
|
||||
|
@@ -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'
|
||||
|
@@ -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)
|
||||
|
2183
lib/twitter/api.py
2183
lib/twitter/api.py
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,18 @@ class TwitterError(Exception):
|
||||
def message(self):
|
||||
'''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
|
||||
|
@@ -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,12 +282,12 @@ class UserStatus(TwitterModel):
|
||||
""" A class representing the UserStatus structure. This is an abbreviated
|
||||
form of the twitter.User object. """
|
||||
|
||||
connections = {'following': False,
|
||||
'followed_by': False,
|
||||
'following_received': False,
|
||||
'following_requested': False,
|
||||
'blocking': False,
|
||||
'muting': False}
|
||||
_connections = {'following': False,
|
||||
'followed_by': False,
|
||||
'following_received': False,
|
||||
'following_requested': False,
|
||||
'blocking': False,
|
||||
'muting': False}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.param_defaults = {
|
||||
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
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'])
|
||||
|
@@ -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,16 +259,17 @@ 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 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:
|
||||
raise TwitterError({'message': 'Videos must be less than 15MB.'})
|
||||
elif media_type not in img_formats and media_type not in video_formats:
|
||||
raise TwitterError({'message': 'Media type could not be determined.'})
|
||||
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:
|
||||
raise TwitterError({'message': 'Videos must be less than 15MB.'})
|
||||
elif media_type not in img_formats and media_type not in video_formats:
|
||||
raise TwitterError({'message': 'Media type could not be determined.'})
|
||||
|
||||
return data_file, filename, file_size, media_type
|
||||
|
||||
@@ -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])
|
||||
|
@@ -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')
|
||||
|
@@ -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
|
||||
|
@@ -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'],
|
||||
|
@@ -403,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'},
|
||||
|
@@ -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'],
|
||||
|
@@ -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)
|
||||
@@ -949,7 +953,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
"""
|
||||
valid_tokens = re.compile(r'(\(|\)|and|or)')
|
||||
conditions_pattern = re.compile(r'{\d+}')
|
||||
|
||||
|
||||
tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()]
|
||||
|
||||
stack = [[]]
|
||||
@@ -960,7 +964,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
close_bracket_next = False
|
||||
nest_and = 0
|
||||
nest_nest_and = 0
|
||||
|
||||
|
||||
for i, x in enumerate(tokens):
|
||||
if open_bracket_next and x == '(':
|
||||
stack[-1].append([])
|
||||
@@ -971,7 +975,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
close_bracket_next = False
|
||||
if nest_and:
|
||||
nest_nest_and += 1
|
||||
|
||||
|
||||
elif close_bracket_next and x == ')':
|
||||
stack.pop()
|
||||
if not stack:
|
||||
@@ -1000,7 +1004,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
if nest_and > nest_nest_and:
|
||||
stack.pop()
|
||||
nest_and -= 1
|
||||
|
||||
|
||||
elif bool_next and x == 'and' and i < len(tokens)-1:
|
||||
stack[-1].append([])
|
||||
stack.append(stack[-1][-1])
|
||||
@@ -1011,7 +1015,7 @@ def parse_condition_logic_string(s, num_cond=0):
|
||||
open_bracket_next = True
|
||||
close_bracket_next = False
|
||||
nest_and += 1
|
||||
|
||||
|
||||
elif bool_next and x == 'or' and i < len(tokens)-1:
|
||||
stack[-1].append(x)
|
||||
cond_next = True
|
||||
|
@@ -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'],
|
||||
|
@@ -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
|
||||
|
||||
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
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -420,7 +420,7 @@ def get_notifiers(notifier_id=None, notify_action=None):
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
|
||||
% (', '.join(notify_actions), where), args=args)
|
||||
|
||||
|
||||
for item in result:
|
||||
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
||||
|
||||
@@ -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):
|
||||
@@ -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:
|
||||
mailserver.quit()
|
||||
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
||||
if mailserver:
|
||||
mailserver.quit()
|
||||
|
||||
return success
|
||||
|
||||
|
@@ -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'],
|
||||
|
@@ -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'],
|
||||
|
@@ -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'),
|
||||
|
@@ -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': '',
|
||||
|
@@ -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'],
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.9"
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.10-beta"
|
||||
|
@@ -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,
|
||||
@@ -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",
|
||||
|
Reference in New Issue
Block a user