mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-07 13:41:15 -07:00
Initial newsletter support
This commit is contained in:
parent
b73d2ff1f7
commit
0f39201774
15 changed files with 2454 additions and 123 deletions
|
@ -2973,6 +2973,9 @@ a .home-platforms-list-cover-face:hover
|
||||||
.stacked-configs > li.new-notification-agent,
|
.stacked-configs > li.new-notification-agent,
|
||||||
.stacked-configs > li.notification-agent,
|
.stacked-configs > li.notification-agent,
|
||||||
.stacked-configs > li.add-notification-agent,
|
.stacked-configs > li.add-notification-agent,
|
||||||
|
.stacked-configs > li.new-newsletter-agent,
|
||||||
|
.stacked-configs > li.newsletter-agent,
|
||||||
|
.stacked-configs > li.add-newsletter-agent,
|
||||||
.stacked-configs > li.mobile-device,
|
.stacked-configs > li.mobile-device,
|
||||||
.stacked-configs > li.add-mobile-device {
|
.stacked-configs > li.add-mobile-device {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3657,38 +3660,58 @@ a:hover .overlay-refresh-image:hover {
|
||||||
}
|
}
|
||||||
#plexpy-notifiers-table .friendly_name,
|
#plexpy-notifiers-table .friendly_name,
|
||||||
#notifier-config-modal span.notifier_id,
|
#notifier-config-modal span.notifier_id,
|
||||||
|
#plexpy-newsletters-table .friendly_name,
|
||||||
|
#newsletter-config-modal span.newsletter_id,
|
||||||
#plexpy-mobile-devices-table .friendly_name,
|
#plexpy-mobile-devices-table .friendly_name,
|
||||||
#mobile-device-config-modal span.notifier_id {
|
#mobile-device-config-modal span.notifier_id {
|
||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
#notifier-config-modal .nav-tabs {
|
#notifier-config-modal .nav-tabs,
|
||||||
|
#newsletter-config-modal .nav-tabs {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
border-bottom: 1px solid #444;
|
border-bottom: 1px solid #444;
|
||||||
}
|
}
|
||||||
#notifier-config-modal .nav-tabs > li {
|
#notifier-config-modal .nav-tabs > li,
|
||||||
|
#newsletter-config-modal .nav-tabs > li {
|
||||||
margin: 0 0 -1px 0;
|
margin: 0 0 -1px 0;
|
||||||
}
|
}
|
||||||
#notifier-config-modal .nav-tabs > li > a {
|
#notifier-config-modal .nav-tabs > li > a,
|
||||||
|
#newsletter-config-modal .nav-tabs > li > a {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
color: #737373;
|
color: #737373;
|
||||||
}
|
}
|
||||||
#notifier-config-modal .nav-tabs > li > a:hover {
|
#notifier-config-modal .nav-tabs > li > a:hover,
|
||||||
|
#newsletter-config-modal .nav-tabs > li > a:hover {
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
background: #222;
|
background: #222;
|
||||||
}
|
}
|
||||||
#notifier-config-modal .nav-tabs > li.active > a,
|
#notifier-config-modal .nav-tabs > li.active > a,
|
||||||
#notifier-config-modal .nav-tabs > li.active > a:hover,
|
#notifier-config-modal .nav-tabs > li.active > a:hover,
|
||||||
#notifier-config-modal .nav-tabs > li.active > a:focus {
|
#notifier-config-modal .nav-tabs > li.active > a:focus,
|
||||||
|
#newsletter-config-modal .nav-tabs > li.active > a,
|
||||||
|
#newsletter-config-modal .nav-tabs > li.active > a:hover,
|
||||||
|
#newsletter-config-modal .nav-tabs > li.active > a:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #222;
|
background: #222;
|
||||||
}
|
}
|
||||||
#notifier-config-modal .nav-tabs > li.active > a,
|
#notifier-config-modal .nav-tabs > li.active > a,
|
||||||
#notifier-config-modal .nav-tabs > li.active > a:hover,
|
#notifier-config-modal .nav-tabs > li.active > a:hover,
|
||||||
#notifier-config-modal .nav-tabs > li.active > a:focus {
|
#notifier-config-modal .nav-tabs > li.active > a:focus,
|
||||||
|
#newsletter-config-modal .nav-tabs > li.active > a,
|
||||||
|
#newsletter-config-modal .nav-tabs > li.active > a:hover,
|
||||||
|
#newsletter-config-modal .nav-tabs > li.active > a:focus {
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
}
|
}
|
||||||
|
#newsletter-config-modal #cron-widget select.cron-select {
|
||||||
|
width: initial;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
#newsletter-config-modal #cron-widget select.cron-select[name=cron-period] option[value=minute],
|
||||||
|
#newsletter-config-modal #cron-widget select.cron-select[name=cron-period] option[value=hour] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.git-group input.form-control {
|
.git-group input.form-control {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
BIN
data/interfaces/default/images/logo-tautulli-newsletter.png
Normal file
BIN
data/interfaces/default/images/logo-tautulli-newsletter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
1
data/interfaces/default/js/jquery-cron-min.js
vendored
Normal file
1
data/interfaces/default/js/jquery-cron-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
306
data/interfaces/default/newsletter_config.html
Normal file
306
data/interfaces/default/newsletter_config.html
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
<%!
|
||||||
|
from plexpy import helpers
|
||||||
|
%>
|
||||||
|
% if newsletter:
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="newsletter-config-modal-header">${newsletter['agent_label']} Newsletter Settings <small><span class="newsletter_id">(Newsletter ID: ${newsletter['id']})</span></small></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<ul class="nav nav-tabs list-unstyled" role="tablist">
|
||||||
|
<li role="presentation" class="active"><a href="#tabs-config" aria-controls="tabs-config" role="tab" data-toggle="tab">Configuration</a></li>
|
||||||
|
<li role="presentation"><a href="#tabs-test_newsletter" aria-controls="tabs-test_newsletter" role="tab" data-toggle="tab">Test Newsletter</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<form action="set_newsletter_config" method="post" class="form" id="set_newsletter_config" data-parsley-validate>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div role="tabpanel" class="tab-pane active" id="tabs-config">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12" style="margin-bottom: 10px; padding-bottom: 0x; border-bottom: 1px solid #444;">
|
||||||
|
<div class="checkbox" style="margin-bottom: 20px;">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-id="active_value" class="checkboxes" value="1" ${helpers.checked(newsletter['active'])}> Enable the newsletter
|
||||||
|
</label>
|
||||||
|
<input type="hidden" id="active_value" name="active" value="${newsletter['active']}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cron">Schedule</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div id="cron-widget"></div>
|
||||||
|
<input type="hidden" id="cron_value" name="cron" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">Set the schedule for the newsletter</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<input type="hidden" id="newsletter_id" name="newsletter_id" value="${newsletter['id']}" />
|
||||||
|
<input type="hidden" id="agent_id" name="agent_id" value="${newsletter['agent_id']}" />
|
||||||
|
% for item in newsletter['config_options']:
|
||||||
|
% if item['input_type'] == 'help':
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${item['label']}</label>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'text' or item['input_type'] == 'password':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'number':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'button':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'checkbox':
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${helpers.checked(item['value'])}> ${item['label']}
|
||||||
|
</label>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'select':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||||
|
% for key, value in sorted(item['select_options'].iteritems()):
|
||||||
|
% if key == item['value']:
|
||||||
|
<option value="${key}" selected>${value}</option>
|
||||||
|
% else:
|
||||||
|
<option value="${key}">${value}</option>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #444;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="friendly_name">Description</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${newsletter['friendly_name']}" size="30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">Optional: Enter a description to help identify this newsletter in the newsletters list.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-test_newsletter">
|
||||||
|
<label>Preview Newsletter</label>
|
||||||
|
<p class="help-block">
|
||||||
|
Preview the ${newsletter['agent_label']} newsletter.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="button" class="btn btn-bright" id="preview_newsletter" name="preview_newsletter" value="Preview ${newsletter['agent_label']} Newsletter">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Test Newsletter</label>
|
||||||
|
<p class="help-block">
|
||||||
|
Test if the ${newsletter['agent_label']} newsletter is working. Check the <a href="logs">logs</a> for troubleshooting.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="button" class="btn btn-bright" id="test_newsletter" name="test_newsletter" value="Test ${newsletter['agent_label']} Newsletter">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" id="delete-newsletter-item" class="btn btn-danger btn-edit" style="float:left;" value="Delete">
|
||||||
|
<input type="button" id="duplicate-newsletter-item" class="btn btn-dark btn-edit" style="float:left;" value="Duplicate">
|
||||||
|
<input type="button" id="save-newsletter-item" class="btn btn-bright" value="Save">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="${http_root}js/jquery-cron-min.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
$('#newsletter-config-modal').unbind('hidden.bs.modal');
|
||||||
|
|
||||||
|
$('#cron-widget').cron({
|
||||||
|
initial: "${newsletter['cron']}",
|
||||||
|
classes: "form-control cron-select",
|
||||||
|
onChange: function() {
|
||||||
|
$("#cron_value").val($(this).cron("value"));
|
||||||
|
}
|
||||||
|
}); // apply cron with default options
|
||||||
|
|
||||||
|
function reloadModal() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_newsletter_config_modal',
|
||||||
|
data: { newsletter_id: '${newsletter["id"]}' },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$('#newsletter-config-modal').html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCallback(jqXHR) {
|
||||||
|
if (jqXHR) {
|
||||||
|
var result = $.parseJSON(jqXHR.responseText);
|
||||||
|
var msg = result.message;
|
||||||
|
if (result.result == 'success') {
|
||||||
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||||
|
} else {
|
||||||
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewslettersTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCallback() {
|
||||||
|
$('#newsletter-config-modal').modal('hide');
|
||||||
|
getNewslettersTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function duplicateCallback(result) {
|
||||||
|
// Set new newsletter id
|
||||||
|
$('#newsletter_id').val(result.newsletter_id);
|
||||||
|
// Clear friendly name
|
||||||
|
$('#friendly_name').val("");
|
||||||
|
|
||||||
|
saveNewsletter();
|
||||||
|
|
||||||
|
$('#newsletter-config-modal').on('hidden.bs.modal', function () {
|
||||||
|
loadNewsletterConfig(result.newsletter_id);
|
||||||
|
});
|
||||||
|
$('#newsletter-config-modal').modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNewsletter() {
|
||||||
|
// Trim all text inputs before saving
|
||||||
|
$('input[type=text]').val(function(_, value) {
|
||||||
|
return $.trim(value);
|
||||||
|
});
|
||||||
|
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, true, saveCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#delete-newsletter-item').click(function () {
|
||||||
|
var msg = 'Are you sure you want to delete this <strong>${newsletter["agent_label"]}</strong> newsletter?';
|
||||||
|
var url = 'delete_newsletter';
|
||||||
|
confirmAjaxCall(url, msg, { newsletter_id: '${newsletter["id"]}' }, null, deleteCallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#duplicate-newsletter-item').click(function() {
|
||||||
|
var msg = 'Are you sure you want to duplicate this <strong>${newsletter["agent_label"]}</strong> newsletter?';
|
||||||
|
var url = 'add_newsletter_config';
|
||||||
|
confirmAjaxCall(url, msg, { agent_id: '${newsletter["agent_id"]}' }, null, duplicateCallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#save-newsletter-item').click(function () {
|
||||||
|
saveNewsletter();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#preview_newsletter').click(function () {
|
||||||
|
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, false, previewNewsletter);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#test_newsletter').click(function () {
|
||||||
|
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, false, sendTestNewsletter);
|
||||||
|
});
|
||||||
|
|
||||||
|
function previewNewsletter() {
|
||||||
|
window.open('preview_newsletter?newsletter_id=${newsletter["id"]}');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTestNewsletter() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'send_newsletter',
|
||||||
|
data: {
|
||||||
|
newsletter_id: '${newsletter["id"]}',
|
||||||
|
test: true
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
if (xhr.responseText.indexOf('sent') > -1) {
|
||||||
|
var msg = '<i class="fa fa-check"></i> ' + xhr.responseText;
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
} else {
|
||||||
|
var msg = '<i class="fa fa-times"></i> ' + xhr.responseText;
|
||||||
|
showMsg(msg, false, true, 2000, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("${', '.join(['#' + c['name'] for c in newsletter['config_options'] if c.get('refresh')])}").on('change', function () {
|
||||||
|
// Reload modal to update certain fields
|
||||||
|
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, false, reloadModal);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Never send checkbox values directly, always substitute value in hidden input.
|
||||||
|
$('.checkboxes').click(function () {
|
||||||
|
var configToggle = $(this).data('id');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$('#'+configToggle).val(1);
|
||||||
|
} else {
|
||||||
|
$('#'+configToggle).val(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
% else:
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="newsletter-config-modal-header">Error</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center">
|
||||||
|
<strong>
|
||||||
|
<i class="fa fa-exclamation-circle"></i> Failed to retrieve newsletter configuration. Check the <a href="logs">logs</a> for more info.
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
42
data/interfaces/default/newsletters_table.html
Normal file
42
data/interfaces/default/newsletters_table.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<%doc>
|
||||||
|
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
||||||
|
|
||||||
|
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
||||||
|
|
||||||
|
Filename: newsletters_table.html
|
||||||
|
Version: 0.1
|
||||||
|
|
||||||
|
DOCUMENTATION :: END
|
||||||
|
</%doc>
|
||||||
|
|
||||||
|
<ul class="stacked-configs list-unstyled">
|
||||||
|
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
|
||||||
|
<li class="newsletter-agent" data-id="${newsletter['id']}">
|
||||||
|
<span>
|
||||||
|
<span class="toggle-left trigger-tooltip ${'active' if newsletter['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Newsletter ${'active' if newsletter['active'] else 'inactive'}"><i class="fa fa-lg fa-newspaper-o"></i></span>
|
||||||
|
% if newsletter['friendly_name']:
|
||||||
|
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
|
||||||
|
% else:
|
||||||
|
${newsletter['agent_label']} <span class="friendly_name">(${newsletter['id']})</span>
|
||||||
|
% endif
|
||||||
|
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
<li class="add-newsletter-agent" id="add-newsletter-agent" data-target="#add-newsletter-modal" data-toggle="modal">
|
||||||
|
<span>
|
||||||
|
<span class="toggle-left"><i class="fa fa-lg fa-newspaper-o"></i></span> Add a new newsletter agent
|
||||||
|
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Load newsletter config modal
|
||||||
|
$(".newsletter-agent").click(function () {
|
||||||
|
var newsletter_id = $(this).data('id');
|
||||||
|
loadNewsletterConfig(newsletter_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.trigger-tooltip').tooltip();
|
||||||
|
</script>
|
|
@ -7,8 +7,6 @@
|
||||||
sorted(user_emails, key=lambda u: u['user'])
|
sorted(user_emails, key=lambda u: u['user'])
|
||||||
%>
|
%>
|
||||||
% if notifier:
|
% if notifier:
|
||||||
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
|
|
||||||
<link href="${http_root}css/selectize.min.css" rel="stylesheet" />
|
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
@ -167,7 +165,7 @@
|
||||||
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
||||||
</p>
|
</p>
|
||||||
<div id="condition-widget"></div>
|
<div id="condition-widget"></div>
|
||||||
<input type="hidden" name="custom_conditions" id="custom_conditions" />
|
<input type="hidden" id="custom_conditions" name="custom_conditions" />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="custom_conditions_logic">Condition Logic</label>
|
<label for="custom_conditions_logic">Condition Logic</label>
|
||||||
|
@ -425,7 +423,7 @@
|
||||||
$('#duplicate-notifier-item').click(function() {
|
$('#duplicate-notifier-item').click(function() {
|
||||||
var msg = 'Are you sure you want to duplicate this <strong>${notifier["agent_label"]}</strong> notification agent?';
|
var msg = 'Are you sure you want to duplicate this <strong>${notifier["agent_label"]}</strong> notification agent?';
|
||||||
var url = 'add_notifier_config';
|
var url = 'add_notifier_config';
|
||||||
confirmAjaxCall(url, msg, { agent_id: "${notifier['agent_id']}" }, null, duplicateCallback);
|
confirmAjaxCall(url, msg, { agent_id: '${notifier["agent_id"]}' }, null, duplicateCallback);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#save-notifier-item').click(function () {
|
$('#save-notifier-item').click(function () {
|
||||||
|
@ -767,10 +765,10 @@
|
||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
if (xhr.responseText.indexOf('sent') > -1) {
|
if (xhr.responseText.indexOf('sent') > -1) {
|
||||||
msg = '<i class="fa fa-check"></i> ' + xhr.responseText;
|
var msg = '<i class="fa fa-check"></i> ' + xhr.responseText;
|
||||||
showMsg(msg, false, true, 2000);
|
showMsg(msg, false, true, 2000);
|
||||||
} else {
|
} else {
|
||||||
msg = '<i class="fa fa-times"></i> ' + xhr.responseText;
|
var msg = '<i class="fa fa-times"></i> ' + xhr.responseText;
|
||||||
showMsg(msg, false, true, 2000, true);
|
showMsg(msg, false, true, 2000, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import common, notifiers
|
from plexpy import common, notifiers, newsletters
|
||||||
from plexpy.helpers import anon_url, checked
|
from plexpy.helpers import anon_url, checked
|
||||||
|
|
||||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'])
|
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'])
|
||||||
|
available_newsletter_agents = sorted(newsletters.available_newsletter_agents(), key=lambda k: k['label'])
|
||||||
%>
|
%>
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
</%def>
|
</%def>
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
<li role="presentation"><a href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
|
<li role="presentation"><a href="#tabs-plex_media_server" aria-controls="tabs-plex_media_server" role="tab" data-toggle="tab">Plex Media Server</a></li>
|
||||||
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications</a></li>
|
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications</a></li>
|
||||||
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
|
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
|
||||||
|
<li role="presentation"><a href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
|
||||||
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
|
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
|
||||||
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
|
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -977,6 +979,23 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_agents">
|
||||||
|
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Newsletter Agents</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-block">
|
||||||
|
Add a new newsletter agent, or configure an existing newsletter agent by clicking the settings icon on the right.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<div id="plexpy-newsletters-table">
|
||||||
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading newsletter agents...</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
|
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
|
@ -1246,7 +1265,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="add-newsletter-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="add-newsletter-modal">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title">Add a Newsletter Agent</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<ul class="stacked-configs list-unstyled">
|
||||||
|
% for agent in available_newsletter_agents:
|
||||||
|
<li class="new-newsletter-agent" data-id="${agent['id']}">
|
||||||
|
<span>${agent['label']}</span>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Cancel">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="notifier-config-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="notifier-config-modal"></div>
|
<div id="notifier-config-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="notifier-config-modal"></div>
|
||||||
|
<div id="newsletter-config-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="newsletter-config-modal"></div>
|
||||||
<div id="notify-text-sub-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="notify-text-sub-modal">
|
<div id="notify-text-sub-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="notify-text-sub-modal">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -1536,6 +1584,29 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNewslettersTable() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_newsletters_table',
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#plexpy-newsletters-table").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNewsletterConfig(newsletter_id) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_newsletter_config_modal',
|
||||||
|
data: { newsletter_id: newsletter_id },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#newsletter-config-modal").html(xhr.responseText).modal('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getMobileDevicesTable() {
|
function getMobileDevicesTable() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_mobile_devices_table',
|
url: 'get_mobile_devices_table',
|
||||||
|
@ -1608,6 +1679,7 @@ $(document).ready(function() {
|
||||||
getConfigurationTable();
|
getConfigurationTable();
|
||||||
getSchedulerTable();
|
getSchedulerTable();
|
||||||
getNotifiersTable();
|
getNotifiersTable();
|
||||||
|
getNewslettersTable();
|
||||||
getMobileDevicesTable();
|
getMobileDevicesTable();
|
||||||
loadUpdateDistros();
|
loadUpdateDistros();
|
||||||
settingsChanged = false;
|
settingsChanged = false;
|
||||||
|
@ -1691,6 +1763,7 @@ $(document).ready(function() {
|
||||||
getConfigurationTable();
|
getConfigurationTable();
|
||||||
getSchedulerTable();
|
getSchedulerTable();
|
||||||
getNotifiersTable();
|
getNotifiersTable();
|
||||||
|
getNewslettersTable();
|
||||||
getMobileDevicesTable();
|
getMobileDevicesTable();
|
||||||
|
|
||||||
$('#changelog-modal-link').on('click', function (e) {
|
$('#changelog-modal-link').on('click', function (e) {
|
||||||
|
@ -2304,6 +2377,28 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add a new newsletter agent
|
||||||
|
$('.new-newsletter-agent').click(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: 'add_newsletter_config',
|
||||||
|
data: { agent_id: $(this).data('id') },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
var result = $.parseJSON(xhr.responseText);
|
||||||
|
var msg = result.message;
|
||||||
|
$('#add-newsletter-modal').modal('hide');
|
||||||
|
if (result.result == 'success') {
|
||||||
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||||
|
loadNewsletterConfig(result.newsletter_id);
|
||||||
|
} else {
|
||||||
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true);
|
||||||
|
}
|
||||||
|
getNewslettersTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function apiEnabled() {
|
function apiEnabled() {
|
||||||
var api_enabled = $('#api_enabled').prop('checked');
|
var api_enabled = $('#api_enabled').prop('checked');
|
||||||
$('#app_api_msg').toggle(!(api_enabled));
|
$('#app_api_msg').toggle(!(api_enabled));
|
||||||
|
|
978
data/interfaces/newsletters/recently_added.html
Normal file
978
data/interfaces/newsletters/recently_added.html
Normal file
|
@ -0,0 +1,978 @@
|
||||||
|
% if recently_added:
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width"/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<title>Tautulli ${title} Newsletter</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 1046px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 1046px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1046px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #282A2D;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #282A2D;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
table[class=body] .dates {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
table[class=body] h2 {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
table[class=body] h2 .count-units {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
table[class=body] h2 .sub-header,
|
||||||
|
table[class=body] h2 .sub-header-count {
|
||||||
|
float: left !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER
|
||||||
|
------------------------------------- */
|
||||||
|
.header {
|
||||||
|
background: url(images/logo-tautulli-newsletter.png) center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
.dates {
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
/* -------------------------------------
|
||||||
|
MEDIA SECTIONS
|
||||||
|
------------------------------------- */
|
||||||
|
h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 20px 10px 0 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h2:before {
|
||||||
|
border-top: 1px solid #E5A00D;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
top: -20px;
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.section_type {
|
||||||
|
height: 30px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
color: #E9A049;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.count-units {
|
||||||
|
color: #aaaaaa;
|
||||||
|
font-size: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.section-top-info-container .top-title {
|
||||||
|
font-size: 17px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.section-top-info-container .star-rating {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 84px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
.section-top-info-container .star-rating .star-rating-empty {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.section-top-info-container .star-rating .star-rating-full {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: #E9A049;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
MEDIA CARDS
|
||||||
|
------------------------------------- */
|
||||||
|
.card-instance {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
height: 235px;
|
||||||
|
min-width: 350px;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
.card-instance-album {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
height: 160px;
|
||||||
|
min-width: 350px;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
.card-container {
|
||||||
|
height: 235px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-container-album {
|
||||||
|
height: 160px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-background-overlay {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-wrap: nowrap;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
}
|
||||||
|
.card-background {
|
||||||
|
background-color: #282828;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
height: 235px;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.40;
|
||||||
|
-webkit-filter: blur(3px);
|
||||||
|
filter: blur(3px);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.card-background-album {
|
||||||
|
background-color: #282828;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
height: 160px;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.40;
|
||||||
|
-webkit-filter: blur(3px);
|
||||||
|
filter: blur(3px);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.card-poster-container {
|
||||||
|
background-color: #282828;
|
||||||
|
height: 225px;
|
||||||
|
width: 150px;
|
||||||
|
margin-right: 5px;
|
||||||
|
-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
-webkit-flex-shrink: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.card-poster {
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
height: 225px;
|
||||||
|
width: 150px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.card-cover-container {
|
||||||
|
background-color: #282828;
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
margin-right: 5px;
|
||||||
|
-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.1);
|
||||||
|
-webkit-flex-shrink: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.card-cover {
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.card-info-container {
|
||||||
|
height: 225px;
|
||||||
|
width: 385px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-flex-grow: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.card-info-container-album {
|
||||||
|
height: 150px;
|
||||||
|
width: 385px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-flex-grow: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.card-info {
|
||||||
|
height: 225px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.card-info-album {
|
||||||
|
height: 150px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.card-info-title {
|
||||||
|
padding: 5px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,.1);
|
||||||
|
}
|
||||||
|
.card-info-title h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-info-title h4 a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.card-info-title h4 a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #E9A049;
|
||||||
|
}
|
||||||
|
.card-info-tagline {
|
||||||
|
padding: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-info-episode {
|
||||||
|
padding: 5px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-info-summary {
|
||||||
|
padding: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 130px;
|
||||||
|
}
|
||||||
|
.card-info-summary-episode {
|
||||||
|
padding: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 95px;
|
||||||
|
}
|
||||||
|
.card-info-summary-album {
|
||||||
|
padding: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 55px;
|
||||||
|
}
|
||||||
|
.card-info-footer {
|
||||||
|
align-items: baseline;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 5px;
|
||||||
|
width: 325px;
|
||||||
|
}
|
||||||
|
.card-info-footer span.star-rating {
|
||||||
|
margin-right: 0;
|
||||||
|
height: 19px;
|
||||||
|
width: 58px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.card-info-footer span.star-rating .star-rating-empty {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: #aaaaaa;
|
||||||
|
}
|
||||||
|
.card-info-footer span.star-rating .star-rating-full {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: #E9A049;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
a:hover .card-poster,
|
||||||
|
a:hover .card-cover {
|
||||||
|
-webkit-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
|
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
|
box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 10px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: rgba(0, 0, 0, .15);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper newsletter-header">
|
||||||
|
<div class="header"></div>
|
||||||
|
<div class="dates">${start_date} - ${end_date}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
% if recently_added.get('movie'):
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h2>
|
||||||
|
<span class="sub-header">
|
||||||
|
<img src="images/libraries/movie.svg" class="section_type"/> Recently Added Movies:
|
||||||
|
</span>
|
||||||
|
<span class="sub-header-count">
|
||||||
|
<span class="count">${len(recently_added['movie'])}</span> <span class="count-units">movie${'s' if len(recently_added['movie']) > 1 else ''}</span>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
% for movie in recently_added['movie']:
|
||||||
|
<div class="card-instance" style="${'clear: both; float: none; margin: 3px auto; top: 3px;' if loop.index == len(recently_added['movie'])-1 and loop.index % 2 == 0 else ''}">
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-background-overlay">
|
||||||
|
<div class="card-background" style="background-image: url(pms_image_proxy?img=${movie['art']}&width=500&height=280&fallback=art&refresh=true);"></div>
|
||||||
|
<div class="card-poster-container hidden-xs">
|
||||||
|
<a href="${plexpy_config['pms_web_url']}#!/server/${plexpy_config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}">
|
||||||
|
<div class="card-poster" style="background-image: url(pms_image_proxy?img=${movie['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-info-container">
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-info-title">
|
||||||
|
<h4><a href="${plexpy_config['pms_web_url']}#!/server/${plexpy_config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${movie['rating_key']}" title="${movie['title']}">${movie['title']}</a></h4>
|
||||||
|
</div>
|
||||||
|
% if movie['tagline']:
|
||||||
|
<div class="card-info-tagline">
|
||||||
|
${movie['tagline']}
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
<div class="card-info-summary">
|
||||||
|
${movie['summary'][:425] + (movie['summary'][425:] and '...')}
|
||||||
|
</div>
|
||||||
|
<div class="card-info-footer">
|
||||||
|
% if movie['year']:
|
||||||
|
<span class="badge">${movie['year']}</span>
|
||||||
|
% endif
|
||||||
|
% if movie['content_rating']:
|
||||||
|
<span class="badge">${movie['content_rating']}</span>
|
||||||
|
% endif
|
||||||
|
% if movie['duration']:
|
||||||
|
<span class="badge">${int(int(movie['duration'])/60000)} mins</span>
|
||||||
|
% endif
|
||||||
|
% if movie['rating']:
|
||||||
|
<span class="star-rating">
|
||||||
|
<span class="star-rating-empty">☆☆☆☆☆</span>
|
||||||
|
<span class="star-rating-full" style="width: ${float(movie['rating'])/0.1}%">★★★★★</span>
|
||||||
|
</span>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endfor
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
% if recently_added.get('show'):
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h2>
|
||||||
|
<span class="sub-header">
|
||||||
|
<img src="images/libraries/show.svg" class="section_type"/> Recently Added TV Shows:
|
||||||
|
</span>
|
||||||
|
<span class="sub-header-count">
|
||||||
|
<span class="count">${len(recently_added['show'])}</span> <span class="count-units">show${'s' if len(recently_added['show']) > 1 else ''}</span> /
|
||||||
|
<% total_episodes = sum(season['episode_count'] for show in recently_added['show'] for season in show['season']) %>
|
||||||
|
<span class="count">${total_episodes}</span> <span class="count-units">episode${'s' if total > 1 else ''}</span>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
% for show in recently_added['show']:
|
||||||
|
<div class="card-instance" style="${'clear: both; float: none; margin: 3px auto; top: 3px;' if loop.index == len(recently_added['show'])-1 and loop.index % 2 == 0 else ''}">
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-background-overlay">
|
||||||
|
<div class="card-background" style="background-image: url(pms_image_proxy?img=${show['art']}&width=500&height=280&fallback=art&refresh=true);"></div>
|
||||||
|
<div class="card-poster-container hidden-xs">
|
||||||
|
<%
|
||||||
|
if show['season_count'] == 1 and show['season'][0]['episode_count'] == 1:
|
||||||
|
link_rating_key = show['season'][0]['episode'][0]['rating_key']
|
||||||
|
link_title = show['title'] + " - " + show['season'][0]['episode'][0]['title']
|
||||||
|
else:
|
||||||
|
link_rating_key = show['rating_key']
|
||||||
|
link_title = show['title']
|
||||||
|
%>
|
||||||
|
<a href="${plexpy_config['pms_web_url']}#!/server/${plexpy_config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${link_title}">
|
||||||
|
<div class="card-poster" style="background-image: url(pms_image_proxy?img=${show['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-info-container">
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-info-title">
|
||||||
|
<h4><a href="${plexpy_config['pms_web_url']}#!/server/${plexpy_config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${link_rating_key}" title="${link_title}">${show['title']}</a></h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-info-tagline">
|
||||||
|
<% total_show_episodes = sum(s['episode_count'] for s in show['season']) %>
|
||||||
|
${total_show_episodes} new episode${'s' if total_show_episodes > 1 else ''}
|
||||||
|
</div>
|
||||||
|
<div class="card-info-episode">
|
||||||
|
% for season in show['season']:
|
||||||
|
Season ${season['media_index']} -
|
||||||
|
% if season['episode_count'] == 1:
|
||||||
|
Episode ${season['episode'][0]['media_index']} - ${season['episode'][0]['title']}
|
||||||
|
% else:
|
||||||
|
Episodes ${season['episode_range']}
|
||||||
|
% endif
|
||||||
|
<br>
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
<div class="card-info-summary-episode">
|
||||||
|
% if show['season_count'] == 1 and show['season'][0]['episode_count'] == 1:
|
||||||
|
${show['season'][0]['episode'][0]['summary'][:330] + (show['season'][0]['episode'][0]['summary'][330:] and '...')}
|
||||||
|
% else:
|
||||||
|
<% length = max(0, 300 - 50 * (show['season_count'] - 1)) %>
|
||||||
|
% if length:
|
||||||
|
${show['summary'][:length] + (show['summary'][length:] and '...')}
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="card-info-footer">
|
||||||
|
% if show['studio']:
|
||||||
|
<span class="badge">${show['studio']}</span>
|
||||||
|
% endif
|
||||||
|
% if show['year']:
|
||||||
|
<span class="badge">${show['year']}</span>
|
||||||
|
% endif
|
||||||
|
% if show['content_rating']:
|
||||||
|
<span class="badge">${show['content_rating']}</span>
|
||||||
|
% endif
|
||||||
|
% if show['duration']:
|
||||||
|
<span class="badge">${int(int(show['duration'])/60000)} mins</span>
|
||||||
|
% endif
|
||||||
|
% if show['rating']:
|
||||||
|
<span class="star-rating">
|
||||||
|
<span class="star-rating-empty">☆☆☆☆☆</span>
|
||||||
|
<span class="star-rating-full" style="width: ${float(show['rating'])/0.1}%">★★★★★</span>
|
||||||
|
</span>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endfor
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
% if recently_added.get('artist'):
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h2>
|
||||||
|
<span class="sub-header">
|
||||||
|
<img src="images/libraries/artist.svg" class="section_type"/> Recently Added Music:
|
||||||
|
</span>
|
||||||
|
<span class="sub-header-count">
|
||||||
|
<span class="count">${len(recently_added['artist'])}</span> <span class="count-units">artist${'s' if len(recently_added['artist']) > 1 else ''}</span> /
|
||||||
|
<% total_albums = sum(artist['album_count'] for artist in recently_added['artist']) %>
|
||||||
|
<span class="count">${total_albums}</span> <span class="count-units">album${'s' if total > 1 else ''}</span>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<% album_count = 0 %>
|
||||||
|
% for artist in recently_added['artist']:
|
||||||
|
% for album in artist['album']:
|
||||||
|
<% album_count += 1 %>
|
||||||
|
<div class="card-instance-album" style="${'clear: both; float: none; margin: 3px auto; top: 3px;' if album_count == total_albums and album_count % 2 == 1 else ''}">
|
||||||
|
<div class="card-container-album">
|
||||||
|
<div class="card-background-overlay">
|
||||||
|
<div class="card-background-album" style="background-image: url(pms_image_proxy?img=${album['art']}&width=500&height=280&fallback=art&refresh=true);"></div>
|
||||||
|
<div class="card-cover-container hidden-xs">
|
||||||
|
<a href="${plexpy_config['pms_web_url']}#!/server/${plexpy_config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}">
|
||||||
|
<div class="card-cover" style="background-image: url(pms_image_proxy?img=${album['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-info-container-album">
|
||||||
|
<div class="card-info-album">
|
||||||
|
<div class="card-info-title">
|
||||||
|
<h4><a href="${plexpy_config['pms_web_url']}#!/server/${plexpy_config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${album['rating_key']}" title="${album['title']}">${album['title']}</a></h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-info-tagline">
|
||||||
|
${artist['title']} - ${album['track_count']} track${'s' if album['track_count'] > 1 else ''}
|
||||||
|
</div>
|
||||||
|
<% album['summary'] = """Adventure Time follows two best friends: Finn (a 12-year old boy) and Jake (a wise 28-year-old dog with magical powers), and the surreal adventures undertaken by the duo as they traverse the mystical Land of Ooo. A world built for adventure, Ooo is filled to the brim with various landscapes for the """ %>
|
||||||
|
<div class="card-info-summary-album">
|
||||||
|
${album['summary'][:155] + (album['summary'][155:] and '...')}
|
||||||
|
</div>
|
||||||
|
<div class="card-info-footer">
|
||||||
|
% if album['year']:
|
||||||
|
<span class="badge">${album['year']}</span>
|
||||||
|
% endif
|
||||||
|
% if album['genres']:
|
||||||
|
% for genre in album['genres'][:2]:
|
||||||
|
<span class="badge">${genre}</span>
|
||||||
|
% endfor
|
||||||
|
% endif
|
||||||
|
% if album['rating']:
|
||||||
|
<span class="star-rating">
|
||||||
|
<span class="star-rating-empty">☆☆☆☆☆</span>
|
||||||
|
<span class="star-rating-full" style="width: ${float(album['rating'])/0.1}%">★★★★★</span>
|
||||||
|
</span>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endfor
|
||||||
|
% endfor
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Newsletter generated by <a href="http://tautulli.com/">Tautulli</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
% endif
|
|
@ -620,6 +620,14 @@ def dbcheck():
|
||||||
'custom_conditions TEXT, custom_conditions_logic TEXT)'
|
'custom_conditions TEXT, custom_conditions_logic TEXT)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# newsletters table :: This table keeps record of the newsletter settings
|
||||||
|
c_db.execute(
|
||||||
|
'CREATE TABLE IF NOT EXISTS newsletters (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
|
'agent_id INTEGER, agent_name TEXT, agent_label TEXT, '
|
||||||
|
'friendly_name TEXT, newsletter_config TEXT, '
|
||||||
|
'cron TEXT NOT NULL DEFAULT "0 0 * * 0", active INTEGER DEFAULT 0)'
|
||||||
|
)
|
||||||
|
|
||||||
# poster_urls table :: This table keeps record of the notification poster urls
|
# poster_urls table :: This table keeps record of the notification poster urls
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS poster_urls (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS poster_urls (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
|
|
|
@ -965,4 +965,12 @@ def get_plexpy_url(hostname=None):
|
||||||
else:
|
else:
|
||||||
root = ''
|
root = ''
|
||||||
|
|
||||||
return scheme + '://' + hostname + port + root
|
return scheme + '://' + hostname + port + root
|
||||||
|
|
||||||
|
def momentjs_to_arrow(format, duration=False):
|
||||||
|
invalid_formats = ['Mo', 'DDDo', 'do']
|
||||||
|
if duration:
|
||||||
|
invalid_formats += ['A', 'a']
|
||||||
|
for f in invalid_formats:
|
||||||
|
format = format.replace(f, '')
|
||||||
|
return format
|
||||||
|
|
537
plexpy/newsletters.py
Normal file
537
plexpy/newsletters.py
Normal file
|
@ -0,0 +1,537 @@
|
||||||
|
# This file is part of Tautulli.
|
||||||
|
#
|
||||||
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Tautulli is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import json
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import email.utils
|
||||||
|
from itertools import groupby
|
||||||
|
from mako.lookup import TemplateLookup
|
||||||
|
from mako import exceptions
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
import plexpy
|
||||||
|
import database
|
||||||
|
import helpers
|
||||||
|
import logger
|
||||||
|
import notification_handler
|
||||||
|
import pmsconnect
|
||||||
|
import request
|
||||||
|
|
||||||
|
|
||||||
|
AGENT_IDS = {
|
||||||
|
'recently_added': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def available_newsletter_agents():
|
||||||
|
agents = [
|
||||||
|
{
|
||||||
|
'label': 'Recently Added',
|
||||||
|
'name': 'recently_added',
|
||||||
|
'id': AGENT_IDS['recently_added']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return agents
|
||||||
|
|
||||||
|
|
||||||
|
def get_agent_class(agent_id=None, config=None):
|
||||||
|
if str(agent_id).isdigit():
|
||||||
|
agent_id = int(agent_id)
|
||||||
|
|
||||||
|
if agent_id == 0:
|
||||||
|
return RecentlyAdded(config=config)
|
||||||
|
else:
|
||||||
|
return Newsletter(config=config)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_newsletter_agents():
|
||||||
|
return tuple(a['name'] for a in sorted(available_newsletter_agents(), key=lambda k: k['label']))
|
||||||
|
|
||||||
|
|
||||||
|
def get_newsletters(newsletter_id=None):
|
||||||
|
where = where_id = ''
|
||||||
|
args = []
|
||||||
|
|
||||||
|
if newsletter_id:
|
||||||
|
where = 'WHERE '
|
||||||
|
if newsletter_id:
|
||||||
|
where_id += 'id = ?'
|
||||||
|
args.append(newsletter_id)
|
||||||
|
where += ' AND '.join([w for w in [where_id] if w])
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
result = db.select('SELECT id, agent_id, agent_name, agent_label, '
|
||||||
|
'friendly_name, active FROM newsletters %s' % where, args=args)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def delete_newsletter(newsletter_id=None):
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
if str(newsletter_id).isdigit():
|
||||||
|
logger.debug(u"Tautulli Newsletters :: Deleting newsletter_id %s from the database."
|
||||||
|
% newsletter_id)
|
||||||
|
result = db.action('DELETE FROM newsletters WHERE id = ?', args=[newsletter_id])
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_newsletter_config(newsletter_id=None):
|
||||||
|
if str(newsletter_id).isdigit():
|
||||||
|
newsletter_id = int(newsletter_id)
|
||||||
|
else:
|
||||||
|
logger.error(u"Tautulli Newsletters :: Unable to retrieve newsletter config: invalid newsletter_id %s."
|
||||||
|
% newsletter_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
result = db.select_single('SELECT * FROM newsletters WHERE id = ?', args=[newsletter_id])
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = json.loads(result.pop('newsletter_config') or '{}')
|
||||||
|
newsletter_agent = get_agent_class(agent_id=result['agent_id'], config=config)
|
||||||
|
newsletter_config = newsletter_agent.return_config_options()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e)
|
||||||
|
return
|
||||||
|
|
||||||
|
result['config'] = config
|
||||||
|
result['config_options'] = newsletter_config
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def add_newsletter_config(agent_id=None, **kwargs):
|
||||||
|
if str(agent_id).isdigit():
|
||||||
|
agent_id = int(agent_id)
|
||||||
|
else:
|
||||||
|
logger.error(u"Tautulli Newsletters :: Unable to add new newsletter: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
agent = next((a for a in available_newsletter_agents() if a['id'] == agent_id), None)
|
||||||
|
|
||||||
|
if not agent:
|
||||||
|
logger.error(u"Tautulli Newsletters :: Unable to retrieve new newsletter agent: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
keys = {'id': None}
|
||||||
|
values = {'agent_id': agent['id'],
|
||||||
|
'agent_name': agent['name'],
|
||||||
|
'agent_label': agent['label'],
|
||||||
|
'friendly_name': '',
|
||||||
|
'newsletter_config': json.dumps(get_agent_class(agent_id=agent['id']).config)
|
||||||
|
}
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
try:
|
||||||
|
db.upsert(table_name='newsletters', key_dict=keys, value_dict=values)
|
||||||
|
newsletter_id = db.last_insert_id()
|
||||||
|
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
|
||||||
|
% (agent['label'], newsletter_id))
|
||||||
|
return newsletter_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
||||||
|
if str(agent_id).isdigit():
|
||||||
|
agent_id = int(agent_id)
|
||||||
|
else:
|
||||||
|
logger.error(u"Tautulli Newsletters :: Unable to set exisiting newsletter: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
agent = next((a for a in available_newsletter_agents() if a['id'] == agent_id), None)
|
||||||
|
|
||||||
|
if not agent:
|
||||||
|
logger.error(u"Tautulli Newsletters :: Unable to retrieve existing newsletter agent: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_prefix = agent['name'] + '_'
|
||||||
|
|
||||||
|
newsletter_config = {k[len(config_prefix):]: kwargs.pop(k)
|
||||||
|
for k in kwargs.keys() if k.startswith(config_prefix)}
|
||||||
|
newsletter_config = get_agent_class(agent['id']).set_config(config=newsletter_config)
|
||||||
|
|
||||||
|
keys = {'id': newsletter_id}
|
||||||
|
values = {'agent_id': agent['id'],
|
||||||
|
'agent_name': agent['name'],
|
||||||
|
'agent_label': agent['label'],
|
||||||
|
'friendly_name': kwargs.get('friendly_name', ''),
|
||||||
|
'newsletter_config': json.dumps(newsletter_config),
|
||||||
|
'cron': kwargs.get('cron'),
|
||||||
|
'active': kwargs.get('active')
|
||||||
|
}
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
try:
|
||||||
|
db.upsert(table_name='newsletters', key_dict=keys, value_dict=values)
|
||||||
|
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
|
||||||
|
% (agent['label'], newsletter_id))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def send_newsletter(newsletter_id=None, newsletter_log_id=None, **kwargs):
|
||||||
|
newsletter_config = get_newsletter_config(newsletter_id=newsletter_id)
|
||||||
|
if newsletter_config:
|
||||||
|
agent = get_agent_class(agent_id=newsletter_config['agent_id'],
|
||||||
|
config=newsletter_config['config'])
|
||||||
|
return agent.send(newsletter_log_id=newsletter_log_id, **kwargs)
|
||||||
|
else:
|
||||||
|
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
|
||||||
|
|
||||||
|
|
||||||
|
def serve_template(templatename, **kwargs):
|
||||||
|
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
||||||
|
template_dir = os.path.join(str(interface_dir), 'newsletters')
|
||||||
|
|
||||||
|
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
template = _hplookup.get_template(templatename)
|
||||||
|
return template.render(**kwargs)
|
||||||
|
except:
|
||||||
|
return exceptions.html_error_template().render()
|
||||||
|
|
||||||
|
|
||||||
|
class Newsletter(object):
|
||||||
|
NAME = ''
|
||||||
|
_DEFAULT_CONFIG = {}
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
|
self.config = {}
|
||||||
|
self.set_config(config)
|
||||||
|
|
||||||
|
def set_config(self, config=None):
|
||||||
|
self.config = self._validate_config(config)
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
def _validate_config(self, config=None):
|
||||||
|
if config is None:
|
||||||
|
return self._DEFAULT_CONFIG
|
||||||
|
|
||||||
|
new_config = {}
|
||||||
|
for k, v in self._DEFAULT_CONFIG.iteritems():
|
||||||
|
if isinstance(v, int):
|
||||||
|
new_config[k] = helpers.cast_to_int(config.get(k, v))
|
||||||
|
else:
|
||||||
|
new_config[k] = config.get(k, v)
|
||||||
|
|
||||||
|
return new_config
|
||||||
|
|
||||||
|
def preview(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def make_request(self, url, method='POST', **kwargs):
|
||||||
|
response, err_msg, req_msg = request.request_response2(url, method, **kwargs)
|
||||||
|
|
||||||
|
if response and not err_msg:
|
||||||
|
logger.info(u"Tautulli Newsletters :: {name} notification sent.".format(name=self.NAME))
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
verify_msg = ""
|
||||||
|
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
||||||
|
verify_msg = " Verify you notification newsletter agent settings are correct."
|
||||||
|
|
||||||
|
logger.error(u"Tautulli Newsletters :: {name} notification failed.{}".format(verify_msg, name=self.NAME))
|
||||||
|
|
||||||
|
if err_msg:
|
||||||
|
logger.error(u"Tautulli Newsletters :: {}".format(err_msg))
|
||||||
|
|
||||||
|
if req_msg:
|
||||||
|
logger.debug(u"Tautulli Newsletters :: Request response: {}".format(req_msg))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def return_config_options(self):
|
||||||
|
config_options = []
|
||||||
|
return config_options
|
||||||
|
|
||||||
|
|
||||||
|
class RecentlyAdded(Newsletter):
|
||||||
|
"""
|
||||||
|
Recently Added Newsletter
|
||||||
|
"""
|
||||||
|
NAME = 'Recently Added'
|
||||||
|
_DEFAULT_CONFIG = {'last_days': 7,
|
||||||
|
'incl_movies': 1,
|
||||||
|
'incl_shows': 1,
|
||||||
|
'incl_artists': 1
|
||||||
|
}
|
||||||
|
_TEMPLATE = 'recently_added.html'
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
|
super(RecentlyAdded, self).__init__(config)
|
||||||
|
|
||||||
|
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
|
||||||
|
|
||||||
|
self.end_time = int(time.time())
|
||||||
|
self.start_time = self.end_time - self.config['last_days']*24*60*60
|
||||||
|
self.end_date = arrow.get(self.end_time).format(date_format)
|
||||||
|
self.start_date = arrow.get(self.start_time).format(date_format)
|
||||||
|
|
||||||
|
self.plexpy_config = {
|
||||||
|
'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
|
'pms_web_url': plexpy.CONFIG.PMS_WEB_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recently_added = {}
|
||||||
|
|
||||||
|
def _get_recently_added(self, media_type=None):
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
|
||||||
|
recently_added = []
|
||||||
|
done = False
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
while not done:
|
||||||
|
recent_items = pms_connect.get_recently_added_details(start=str(start), count='10', type=media_type)
|
||||||
|
filtered_items = [i for i in recent_items['recently_added']
|
||||||
|
if helpers.cast_to_int(i['added_at']) > self.start_time]
|
||||||
|
if len(filtered_items) < 10:
|
||||||
|
done = True
|
||||||
|
else:
|
||||||
|
start += 10
|
||||||
|
|
||||||
|
recently_added.extend(filtered_items)
|
||||||
|
|
||||||
|
if media_type == 'show':
|
||||||
|
shows_list = []
|
||||||
|
show_rating_keys = []
|
||||||
|
for item in recently_added:
|
||||||
|
if item['media_type'] == 'show':
|
||||||
|
show_rating_key = item['rating_key']
|
||||||
|
elif item['media_type'] == 'season':
|
||||||
|
show_rating_key = item['parent_rating_key']
|
||||||
|
elif item['media_type'] == 'episode':
|
||||||
|
show_rating_key = item['grandparent_rating_key']
|
||||||
|
|
||||||
|
if show_rating_key in show_rating_keys:
|
||||||
|
continue
|
||||||
|
|
||||||
|
show_metadata = pms_connect.get_metadata_details(show_rating_key, media_info=False)
|
||||||
|
children = pms_connect.get_item_children(show_rating_key, get_grandchildren=True)
|
||||||
|
filtered_children = [i for i in children['children_list']
|
||||||
|
if helpers.cast_to_int(i['added_at']) > self.start_time]
|
||||||
|
filtered_children.sort(key=lambda x: x['parent_media_index'])
|
||||||
|
|
||||||
|
seasons = []
|
||||||
|
for k, v in groupby(filtered_children, key=lambda x: x['parent_media_index']):
|
||||||
|
episodes = list(v)
|
||||||
|
num, num00 = notification_handler.format_group_index(
|
||||||
|
[helpers.cast_to_int(d['media_index']) for d in episodes])
|
||||||
|
|
||||||
|
seasons.append({'media_index': k,
|
||||||
|
'episode_range': num00,
|
||||||
|
'episode_count': len(episodes),
|
||||||
|
'episode': episodes})
|
||||||
|
|
||||||
|
num, num00 = notification_handler.format_group_index(
|
||||||
|
[helpers.cast_to_int(d['media_index']) for d in seasons])
|
||||||
|
|
||||||
|
show_metadata['season_range'] = num00
|
||||||
|
show_metadata['season_count'] = len(seasons)
|
||||||
|
show_metadata['season'] = seasons
|
||||||
|
|
||||||
|
shows_list.append(show_metadata)
|
||||||
|
show_rating_keys.append(show_rating_key)
|
||||||
|
|
||||||
|
recently_added = shows_list
|
||||||
|
|
||||||
|
if media_type == 'artist':
|
||||||
|
artists_list = []
|
||||||
|
artist_rating_keys = []
|
||||||
|
for item in recently_added:
|
||||||
|
if item['media_type'] == 'artist':
|
||||||
|
artist_rating_key = item['rating_key']
|
||||||
|
elif item['media_type'] == 'album':
|
||||||
|
artist_rating_key = item['parent_rating_key']
|
||||||
|
elif item['media_type'] == 'track':
|
||||||
|
artist_rating_key = item['grandparent_rating_key']
|
||||||
|
|
||||||
|
if artist_rating_key in artist_rating_keys:
|
||||||
|
continue
|
||||||
|
|
||||||
|
artist_metadata = pms_connect.get_metadata_details(artist_rating_key, media_info=False)
|
||||||
|
children = pms_connect.get_item_children(artist_rating_key)
|
||||||
|
filtered_children = [i for i in children['children_list']
|
||||||
|
if helpers.cast_to_int(i['added_at']) > self.start_time]
|
||||||
|
filtered_children.sort(key=lambda x: x['added_at'])
|
||||||
|
|
||||||
|
albums = []
|
||||||
|
for a in filtered_children:
|
||||||
|
album_metadata = pms_connect.get_metadata_details(a['rating_key'], media_info=False)
|
||||||
|
album_metadata['track_count'] = helpers.cast_to_int(album_metadata['children_count'])
|
||||||
|
albums.append(album_metadata)
|
||||||
|
|
||||||
|
artist_metadata['album_count'] = len(albums)
|
||||||
|
artist_metadata['album'] = albums
|
||||||
|
|
||||||
|
artists_list.append(artist_metadata)
|
||||||
|
artist_rating_keys.append(artist_rating_key)
|
||||||
|
|
||||||
|
recently_added = artists_list
|
||||||
|
|
||||||
|
return recently_added
|
||||||
|
|
||||||
|
def get_recently_added(self):
|
||||||
|
if self.config['incl_movies']:
|
||||||
|
self.recently_added['movie'] = self._get_recently_added('movie')
|
||||||
|
if self.config['incl_shows']:
|
||||||
|
self.recently_added['show'] = self._get_recently_added('show')
|
||||||
|
if self.config['incl_artists']:
|
||||||
|
self.recently_added['artist'] = self._get_recently_added('artist')
|
||||||
|
|
||||||
|
return self.recently_added
|
||||||
|
|
||||||
|
def preview(self, **kwargs):
|
||||||
|
self.get_recently_added()
|
||||||
|
|
||||||
|
return serve_template(
|
||||||
|
templatename=self._TEMPLATE,
|
||||||
|
title=self.NAME,
|
||||||
|
recently_added=self.recently_added,
|
||||||
|
start_date=self.start_date,
|
||||||
|
end_date=self.end_date,
|
||||||
|
plexpy_config=self.plexpy_config
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(self, **kwargs):
|
||||||
|
if not subject or not body:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.config['incl_subject']:
|
||||||
|
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||||
|
else:
|
||||||
|
text = body.encode("utf-8")
|
||||||
|
|
||||||
|
data = {'content': text}
|
||||||
|
if self.config['username']:
|
||||||
|
data['username'] = self.config['username']
|
||||||
|
if self.config['avatar_url']:
|
||||||
|
data['avatar_url'] = self.config['avatar_url']
|
||||||
|
if self.config['tts']:
|
||||||
|
data['tts'] = True
|
||||||
|
|
||||||
|
if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
|
||||||
|
# Grab formatted metadata
|
||||||
|
pretty_metadata = PrettyMetadata(kwargs['parameters'])
|
||||||
|
|
||||||
|
if pretty_metadata.media_type == 'movie':
|
||||||
|
provider = self.config['movie_provider']
|
||||||
|
elif pretty_metadata.media_type in ('show', 'season', 'episode'):
|
||||||
|
provider = self.config['tv_provider']
|
||||||
|
elif pretty_metadata.media_type in ('artist', 'album', 'track'):
|
||||||
|
provider = self.config['music_provider']
|
||||||
|
else:
|
||||||
|
provider = None
|
||||||
|
|
||||||
|
poster_url = pretty_metadata.get_poster_url()
|
||||||
|
provider_name = pretty_metadata.get_provider_name(provider)
|
||||||
|
provider_link = pretty_metadata.get_provider_link(provider)
|
||||||
|
title = pretty_metadata.get_title('\xc2\xb7'.decode('utf8'))
|
||||||
|
description = pretty_metadata.get_description()
|
||||||
|
plex_url = pretty_metadata.get_plex_url()
|
||||||
|
|
||||||
|
# Build Discord post attachment
|
||||||
|
attachment = {'title': title
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config['color']:
|
||||||
|
hex_match = re.match(r'^#([0-9a-fA-F]{3}){1,2}$', self.config['color'])
|
||||||
|
if hex_match:
|
||||||
|
hex = hex_match.group(0).lstrip('#')
|
||||||
|
hex = ''.join(h * 2 for h in hex) if len(hex) == 3 else hex
|
||||||
|
attachment['color'] = helpers.hex_to_int(hex)
|
||||||
|
|
||||||
|
if self.config['incl_thumbnail']:
|
||||||
|
attachment['thumbnail'] = {'url': poster_url}
|
||||||
|
else:
|
||||||
|
attachment['image'] = {'url': poster_url}
|
||||||
|
|
||||||
|
if self.config['incl_description'] or pretty_metadata.media_type in ('artist', 'album', 'track'):
|
||||||
|
attachment['description'] = description
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
if provider_link:
|
||||||
|
attachment['url'] = provider_link
|
||||||
|
fields.append({'name': 'View Details',
|
||||||
|
'value': '[%s](%s)' % (provider_name, provider_link.encode('utf-8')),
|
||||||
|
'inline': True})
|
||||||
|
if self.config['incl_pmslink']:
|
||||||
|
fields.append({'name': 'View Details',
|
||||||
|
'value': '[Plex Web](%s)' % plex_url.encode('utf-8'),
|
||||||
|
'inline': True})
|
||||||
|
if fields:
|
||||||
|
attachment['fields'] = fields
|
||||||
|
|
||||||
|
data['embeds'] = [attachment]
|
||||||
|
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
params = {'wait': True}
|
||||||
|
|
||||||
|
return self.make_request(self.config['hook'], params=params, headers=headers, json=data)
|
||||||
|
|
||||||
|
def return_config_options(self):
|
||||||
|
config_option = [{'label': 'Number of Days',
|
||||||
|
'value': self.config['last_days'],
|
||||||
|
'name': 'recently_added_last_days',
|
||||||
|
'description': 'The past number of days to include in the newsletter.',
|
||||||
|
'input_type': 'number'
|
||||||
|
},
|
||||||
|
{'label': 'Include Movies',
|
||||||
|
'value': self.config['incl_movies'],
|
||||||
|
'description': 'Include recently added movies in the newsletter.',
|
||||||
|
'name': 'recently_added_incl_movies',
|
||||||
|
'input_type': 'checkbox'
|
||||||
|
},
|
||||||
|
{'label': 'Include TV Shows',
|
||||||
|
'value': self.config['incl_shows'],
|
||||||
|
'description': 'Include recently added TV shows in the newsletter.',
|
||||||
|
'name': 'recently_added_incl_shows',
|
||||||
|
'input_type': 'checkbox'
|
||||||
|
},
|
||||||
|
{'label': 'Include Music',
|
||||||
|
'value': self.config['incl_artists'],
|
||||||
|
'description': 'Include recently added music in the newsletter.',
|
||||||
|
'name': 'recently_added_incl_artists',
|
||||||
|
'input_type': 'checkbox'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return config_option
|
|
@ -448,9 +448,9 @@ def set_notify_success(notification_id):
|
||||||
|
|
||||||
def build_media_notify_params(notify_action=None, session=None, timeline=None, manual_trigger=False, **kwargs):
|
def build_media_notify_params(notify_action=None, session=None, timeline=None, manual_trigger=False, **kwargs):
|
||||||
# Get time formats
|
# Get time formats
|
||||||
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
|
||||||
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
time_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT)
|
||||||
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
|
duration_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT, duration=True)
|
||||||
|
|
||||||
# Get metadata for the item
|
# Get metadata for the item
|
||||||
if session:
|
if session:
|
||||||
|
|
|
@ -338,7 +338,7 @@ def get_agent_class(agent_id=None, config=None):
|
||||||
agent_id = int(agent_id)
|
agent_id = int(agent_id)
|
||||||
|
|
||||||
if agent_id == 0:
|
if agent_id == 0:
|
||||||
return GROWL(config=config,)
|
return GROWL(config=config)
|
||||||
elif agent_id == 1:
|
elif agent_id == 1:
|
||||||
return PROWL(config=config)
|
return PROWL(config=config)
|
||||||
elif agent_id == 2:
|
elif agent_id == 2:
|
||||||
|
@ -419,8 +419,8 @@ def get_notifiers(notifier_id=None, notify_action=None):
|
||||||
|
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
|
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
|
||||||
% (', '.join(notify_actions), where), args=args)
|
% (', '.join(notify_actions), where), args=args)
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
||||||
|
|
||||||
|
@ -431,9 +431,9 @@ def delete_notifier(notifier_id=None):
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
|
|
||||||
if str(notifier_id).isdigit():
|
if str(notifier_id).isdigit():
|
||||||
logger.debug(u"Tautulli Notifiers :: Deleting notifier_id %s from the database." % notifier_id)
|
logger.debug(u"Tautulli Notifiers :: Deleting notifier_id %s from the database."
|
||||||
result = db.action('DELETE FROM notifiers WHERE id = ?',
|
% notifier_id)
|
||||||
args=[notifier_id])
|
result = db.action('DELETE FROM notifiers WHERE id = ?', args=[notifier_id])
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -443,12 +443,13 @@ def get_notifier_config(notifier_id=None):
|
||||||
if str(notifier_id).isdigit():
|
if str(notifier_id).isdigit():
|
||||||
notifier_id = int(notifier_id)
|
notifier_id = int(notifier_id)
|
||||||
else:
|
else:
|
||||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve notifier config: invalid notifier_id %s." % notifier_id)
|
logger.error(u"Tautulli Notifiers :: Unable to retrieve notifier config: invalid notifier_id %s."
|
||||||
|
% notifier_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
result = db.select_single('SELECT * FROM notifiers WHERE id = ?',
|
result = db.select_single('SELECT * FROM notifiers WHERE id = ?', args=[notifier_id])
|
||||||
args=[notifier_id])
|
|
||||||
if not result:
|
if not result:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -490,13 +491,15 @@ def add_notifier_config(agent_id=None, **kwargs):
|
||||||
if str(agent_id).isdigit():
|
if str(agent_id).isdigit():
|
||||||
agent_id = int(agent_id)
|
agent_id = int(agent_id)
|
||||||
else:
|
else:
|
||||||
logger.error(u"Tautulli Notifiers :: Unable to add new notifier: invalid agent_id %s." % agent_id)
|
logger.error(u"Tautulli Notifiers :: Unable to add new notifier: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
|
agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
|
||||||
|
|
||||||
if not agent:
|
if not agent:
|
||||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve new notification agent: invalid agent_id %s." % agent_id)
|
logger.error(u"Tautulli Notifiers :: Unable to retrieve new notification agent: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
keys = {'id': None}
|
keys = {'id': None}
|
||||||
|
@ -521,7 +524,8 @@ def add_notifier_config(agent_id=None, **kwargs):
|
||||||
try:
|
try:
|
||||||
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
||||||
notifier_id = db.last_insert_id()
|
notifier_id = db.last_insert_id()
|
||||||
logger.info(u"Tautulli Notifiers :: Added new notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
|
logger.info(u"Tautulli Notifiers :: Added new notification agent: %s (notifier_id %s)."
|
||||||
|
% (agent['label'], notifier_id))
|
||||||
blacklist_logger()
|
blacklist_logger()
|
||||||
return notifier_id
|
return notifier_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -533,13 +537,15 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
||||||
if str(agent_id).isdigit():
|
if str(agent_id).isdigit():
|
||||||
agent_id = int(agent_id)
|
agent_id = int(agent_id)
|
||||||
else:
|
else:
|
||||||
logger.error(u"Tautulli Notifiers :: Unable to set exisiting notifier: invalid agent_id %s." % agent_id)
|
logger.error(u"Tautulli Notifiers :: Unable to set exisiting notifier: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
|
agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
|
||||||
|
|
||||||
if not agent:
|
if not agent:
|
||||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve existing notification agent: invalid agent_id %s." % agent_id)
|
logger.error(u"Tautulli Notifiers :: Unable to retrieve existing notification agent: invalid agent_id %s."
|
||||||
|
% agent_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
notify_actions = get_notify_actions()
|
notify_actions = get_notify_actions()
|
||||||
|
@ -571,7 +577,8 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
try:
|
try:
|
||||||
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
||||||
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
|
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)."
|
||||||
|
% (agent['label'], notifier_id))
|
||||||
blacklist_logger()
|
blacklist_logger()
|
||||||
|
|
||||||
if agent['name'] == 'browser':
|
if agent['name'] == 'browser':
|
||||||
|
@ -743,6 +750,7 @@ class Notifier(object):
|
||||||
_DEFAULT_CONFIG = {}
|
_DEFAULT_CONFIG = {}
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
|
self.config = {}
|
||||||
self.set_config(config)
|
self.set_config(config)
|
||||||
|
|
||||||
def set_config(self, config=None):
|
def set_config(self, config=None):
|
||||||
|
|
|
@ -139,6 +139,22 @@ class PmsConnect(object):
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
def get_metadata_grandchildren(self, rating_key='', output_format=''):
|
||||||
|
"""
|
||||||
|
Return metadata for graandchildren of the request item.
|
||||||
|
|
||||||
|
Parameters required: rating_key { Plex ratingKey }
|
||||||
|
Optional parameters: output_format { dict, json }
|
||||||
|
|
||||||
|
Output: array
|
||||||
|
"""
|
||||||
|
uri = '/library/metadata/' + rating_key + '/grandchildren'
|
||||||
|
request = self.request_handler.make_request(uri=uri,
|
||||||
|
request_type='GET',
|
||||||
|
output_format=output_format)
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
||||||
def get_recently_added(self, start='0', count='0', output_format=''):
|
def get_recently_added(self, start='0', count='0', output_format=''):
|
||||||
"""
|
"""
|
||||||
Return list of recently added items.
|
Return list of recently added items.
|
||||||
|
@ -171,22 +187,6 @@ class PmsConnect(object):
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def get_children_list(self, rating_key='', output_format=''):
|
|
||||||
"""
|
|
||||||
Return list of children in requested library item.
|
|
||||||
|
|
||||||
Parameters required: rating_key { ratingKey of parent }
|
|
||||||
Optional parameters: output_format { dict, json }
|
|
||||||
|
|
||||||
Output: array
|
|
||||||
"""
|
|
||||||
uri = '/library/metadata/' + rating_key + '/children'
|
|
||||||
request = self.request_handler.make_request(uri=uri,
|
|
||||||
request_type='GET',
|
|
||||||
output_format=output_format)
|
|
||||||
|
|
||||||
return request
|
|
||||||
|
|
||||||
def get_children_list_related(self, rating_key='', output_format=''):
|
def get_children_list_related(self, rating_key='', output_format=''):
|
||||||
"""
|
"""
|
||||||
Return list of related children in requested collection item.
|
Return list of related children in requested collection item.
|
||||||
|
@ -470,59 +470,86 @@ class PmsConnect(object):
|
||||||
output = {'recently_added': []}
|
output = {'recently_added': []}
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
recents_main = []
|
||||||
if a.getElementsByTagName('Directory'):
|
if a.getElementsByTagName('Directory'):
|
||||||
recents_main = a.getElementsByTagName('Directory')
|
recents_main += a.getElementsByTagName('Directory')
|
||||||
for item in recents_main:
|
|
||||||
recent_items = {'media_type': helpers.get_xml_attr(item, 'type'),
|
|
||||||
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
|
|
||||||
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
|
|
||||||
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
|
|
||||||
'title': helpers.get_xml_attr(item, 'title'),
|
|
||||||
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
|
||||||
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
|
|
||||||
'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'),
|
|
||||||
'section_id': section_id if section_id else helpers.get_xml_attr(item, 'librarySectionID'),
|
|
||||||
'library_name': helpers.get_xml_attr(item, 'librarySectionTitle'),
|
|
||||||
'year': helpers.get_xml_attr(item, 'year'),
|
|
||||||
'thumb': helpers.get_xml_attr(item, 'thumb'),
|
|
||||||
'parent_thumb': helpers.get_xml_attr(item, 'parentThumb'),
|
|
||||||
'grandparent_thumb': helpers.get_xml_attr(item, 'grandparentThumb'),
|
|
||||||
'added_at': helpers.get_xml_attr(item, 'addedAt'),
|
|
||||||
'child_count': helpers.get_xml_attr(item, 'childCount')
|
|
||||||
}
|
|
||||||
recents_list.append(recent_items)
|
|
||||||
|
|
||||||
if a.getElementsByTagName('Video'):
|
if a.getElementsByTagName('Video'):
|
||||||
recents_main = a.getElementsByTagName('Video')
|
recents_main += a.getElementsByTagName('Video')
|
||||||
for item in recents_main:
|
|
||||||
recent_items = {'media_type': helpers.get_xml_attr(item, 'type'),
|
for m in recents_main:
|
||||||
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
|
directors = []
|
||||||
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
|
writers = []
|
||||||
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
|
actors = []
|
||||||
'title': helpers.get_xml_attr(item, 'title'),
|
genres = []
|
||||||
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
labels = []
|
||||||
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
|
|
||||||
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
|
if m.getElementsByTagName('Director'):
|
||||||
'media_index': helpers.get_xml_attr(item, 'index'),
|
for director in m.getElementsByTagName('Director'):
|
||||||
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
|
directors.append(helpers.get_xml_attr(director, 'tag'))
|
||||||
'section_id': section_id if section_id else helpers.get_xml_attr(item, 'librarySectionID'),
|
|
||||||
'library_name': helpers.get_xml_attr(item, 'librarySectionTitle'),
|
if m.getElementsByTagName('Writer'):
|
||||||
'year': helpers.get_xml_attr(item, 'year'),
|
for writer in m.getElementsByTagName('Writer'):
|
||||||
'thumb': helpers.get_xml_attr(item, 'thumb'),
|
writers.append(helpers.get_xml_attr(writer, 'tag'))
|
||||||
'parent_thumb': helpers.get_xml_attr(item, 'parentThumb'),
|
|
||||||
'grandparent_thumb': helpers.get_xml_attr(item, 'grandparentThumb'),
|
if m.getElementsByTagName('Role'):
|
||||||
'added_at': helpers.get_xml_attr(item, 'addedAt'),
|
for actor in m.getElementsByTagName('Role'):
|
||||||
'child_count': helpers.get_xml_attr(item, 'childCount')
|
actors.append(helpers.get_xml_attr(actor, 'tag'))
|
||||||
}
|
|
||||||
recents_list.append(recent_items)
|
if m.getElementsByTagName('Genre'):
|
||||||
|
for genre in m.getElementsByTagName('Genre'):
|
||||||
|
genres.append(helpers.get_xml_attr(genre, 'tag'))
|
||||||
|
|
||||||
|
if m.getElementsByTagName('Label'):
|
||||||
|
for label in m.getElementsByTagName('Label'):
|
||||||
|
labels.append(helpers.get_xml_attr(label, 'tag'))
|
||||||
|
|
||||||
|
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
|
||||||
|
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
|
||||||
|
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
|
||||||
|
'rating_key': helpers.get_xml_attr(m, 'ratingKey'),
|
||||||
|
'parent_rating_key': helpers.get_xml_attr(m, 'parentRatingKey'),
|
||||||
|
'grandparent_rating_key': helpers.get_xml_attr(m, 'grandparentRatingKey'),
|
||||||
|
'title': helpers.get_xml_attr(m, 'title'),
|
||||||
|
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
||||||
|
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
||||||
|
'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'),
|
||||||
|
'studio': helpers.get_xml_attr(m, 'studio'),
|
||||||
|
'content_rating': helpers.get_xml_attr(m, 'contentRating'),
|
||||||
|
'summary': helpers.get_xml_attr(m, 'summary'),
|
||||||
|
'tagline': helpers.get_xml_attr(m, 'tagline'),
|
||||||
|
'rating': helpers.get_xml_attr(m, 'rating'),
|
||||||
|
'audience_rating': helpers.get_xml_attr(m, 'audienceRating'),
|
||||||
|
'user_rating': helpers.get_xml_attr(m, 'userRating'),
|
||||||
|
'duration': helpers.get_xml_attr(m, 'duration'),
|
||||||
|
'year': helpers.get_xml_attr(m, 'year'),
|
||||||
|
'thumb': helpers.get_xml_attr(m, 'thumb'),
|
||||||
|
'parent_thumb': helpers.get_xml_attr(m, 'parentThumb'),
|
||||||
|
'grandparent_thumb': helpers.get_xml_attr(m, 'grandparentThumb'),
|
||||||
|
'art': helpers.get_xml_attr(m, 'art'),
|
||||||
|
'banner': helpers.get_xml_attr(m, 'banner'),
|
||||||
|
'originally_available_at': helpers.get_xml_attr(m, 'originallyAvailableAt'),
|
||||||
|
'added_at': helpers.get_xml_attr(m, 'addedAt'),
|
||||||
|
'updated_at': helpers.get_xml_attr(m, 'updatedAt'),
|
||||||
|
'last_viewed_at': helpers.get_xml_attr(m, 'lastViewedAt'),
|
||||||
|
'guid': helpers.get_xml_attr(m, 'guid'),
|
||||||
|
'directors': directors,
|
||||||
|
'writers': writers,
|
||||||
|
'actors': actors,
|
||||||
|
'genres': genres,
|
||||||
|
'labels': labels,
|
||||||
|
'full_title': helpers.get_xml_attr(m, 'title'),
|
||||||
|
'child_count': helpers.get_xml_attr(m, 'childCount')
|
||||||
|
}
|
||||||
|
|
||||||
|
recents_list.append(recent_item)
|
||||||
|
|
||||||
output = {'recently_added': sorted(recents_list, key=lambda k: k['added_at'], reverse=True)}
|
output = {'recently_added': sorted(recents_list, key=lambda k: k['added_at'], reverse=True)}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_metadata_details(self, rating_key='', sync_id='', cache_key=None):
|
def get_metadata_details(self, rating_key='', sync_id='', cache_key=None, media_info=True):
|
||||||
"""
|
"""
|
||||||
Return processed and validated metadata list for requested item.
|
Return processed and validated metadata list for requested item.
|
||||||
|
|
||||||
|
@ -662,7 +689,8 @@ class PmsConnect(object):
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
'collections': collections,
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'show':
|
elif metadata_type == 'show':
|
||||||
|
@ -708,7 +736,8 @@ class PmsConnect(object):
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
'collections': collections,
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'season':
|
elif metadata_type == 'season':
|
||||||
|
@ -752,7 +781,8 @@ class PmsConnect(object):
|
||||||
'labels': show_details['labels'],
|
'labels': show_details['labels'],
|
||||||
'collections': show_details['collections'],
|
'collections': show_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title')),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'episode':
|
elif metadata_type == 'episode':
|
||||||
|
@ -796,7 +826,8 @@ class PmsConnect(object):
|
||||||
'labels': show_details['labels'],
|
'labels': show_details['labels'],
|
||||||
'collections': show_details['collections'],
|
'collections': show_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title')),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'artist':
|
elif metadata_type == 'artist':
|
||||||
|
@ -837,7 +868,8 @@ class PmsConnect(object):
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
'collections': collections,
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'album':
|
elif metadata_type == 'album':
|
||||||
|
@ -881,7 +913,8 @@ class PmsConnect(object):
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
'collections': collections,
|
'collections': collections,
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title')),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'track':
|
elif metadata_type == 'track':
|
||||||
|
@ -925,7 +958,8 @@ class PmsConnect(object):
|
||||||
'labels': album_details['labels'],
|
'labels': album_details['labels'],
|
||||||
'collections': album_details['collections'],
|
'collections': album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title')),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'photo_album':
|
elif metadata_type == 'photo_album':
|
||||||
|
@ -966,7 +1000,8 @@ class PmsConnect(object):
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
'collections': collections,
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'photo':
|
elif metadata_type == 'photo':
|
||||||
|
@ -1010,7 +1045,8 @@ class PmsConnect(object):
|
||||||
'labels': photo_album_details['labels'],
|
'labels': photo_album_details['labels'],
|
||||||
'collections': photo_album_details['collections'],
|
'collections': photo_album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title')),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'collection':
|
elif metadata_type == 'collection':
|
||||||
|
@ -1055,7 +1091,8 @@ class PmsConnect(object):
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
'collections': collections,
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif metadata_type == 'clip':
|
elif metadata_type == 'clip':
|
||||||
|
@ -1102,7 +1139,7 @@ class PmsConnect(object):
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if metadata:
|
if metadata and media_info:
|
||||||
medias = []
|
medias = []
|
||||||
media_items = metadata_main.getElementsByTagName('Media')
|
media_items = metadata_main.getElementsByTagName('Media')
|
||||||
for media in media_items:
|
for media in media_items:
|
||||||
|
@ -1873,18 +1910,21 @@ class PmsConnect(object):
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_item_children(self, rating_key=''):
|
def get_item_children(self, rating_key='', get_grandchildren=False):
|
||||||
"""
|
"""
|
||||||
Return processed and validated children list.
|
Return processed and validated children list.
|
||||||
|
|
||||||
Output: array
|
Output: array
|
||||||
"""
|
"""
|
||||||
children_data = self.get_children_list(rating_key, output_format='xml')
|
if get_grandchildren:
|
||||||
|
children_data = self.get_metadata_grandchildren(rating_key, output_format='xml')
|
||||||
|
else:
|
||||||
|
children_data = self.get_metadata_children(rating_key, output_format='xml')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml_head = children_data.getElementsByTagName('MediaContainer')
|
xml_head = children_data.getElementsByTagName('MediaContainer')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_children_list: %s." % e)
|
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_item_children: %s." % e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
children_list = []
|
children_list = []
|
||||||
|
@ -1907,21 +1947,72 @@ class PmsConnect(object):
|
||||||
if a.getElementsByTagName('Track'):
|
if a.getElementsByTagName('Track'):
|
||||||
result_data = a.getElementsByTagName('Track')
|
result_data = a.getElementsByTagName('Track')
|
||||||
|
|
||||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
|
||||||
|
|
||||||
if result_data:
|
if result_data:
|
||||||
for result in result_data:
|
for m in result_data:
|
||||||
children_output = {'section_id': section_id,
|
directors = []
|
||||||
'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
|
writers = []
|
||||||
'parent_rating_key': helpers.get_xml_attr(result, 'parentRatingKey'),
|
actors = []
|
||||||
'media_index': helpers.get_xml_attr(result, 'index'),
|
genres = []
|
||||||
'title': helpers.get_xml_attr(result, 'title'),
|
labels = []
|
||||||
'parent_title': helpers.get_xml_attr(result, 'parentTitle'),
|
|
||||||
'year': helpers.get_xml_attr(result, 'year'),
|
if m.getElementsByTagName('Director'):
|
||||||
'thumb': helpers.get_xml_attr(result, 'thumb'),
|
for director in m.getElementsByTagName('Director'):
|
||||||
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
|
directors.append(helpers.get_xml_attr(director, 'tag'))
|
||||||
'duration': helpers.get_xml_attr(result, 'duration')
|
|
||||||
}
|
if m.getElementsByTagName('Writer'):
|
||||||
|
for writer in m.getElementsByTagName('Writer'):
|
||||||
|
writers.append(helpers.get_xml_attr(writer, 'tag'))
|
||||||
|
|
||||||
|
if m.getElementsByTagName('Role'):
|
||||||
|
for actor in m.getElementsByTagName('Role'):
|
||||||
|
actors.append(helpers.get_xml_attr(actor, 'tag'))
|
||||||
|
|
||||||
|
if m.getElementsByTagName('Genre'):
|
||||||
|
for genre in m.getElementsByTagName('Genre'):
|
||||||
|
genres.append(helpers.get_xml_attr(genre, 'tag'))
|
||||||
|
|
||||||
|
if m.getElementsByTagName('Label'):
|
||||||
|
for label in m.getElementsByTagName('Label'):
|
||||||
|
labels.append(helpers.get_xml_attr(label, 'tag'))
|
||||||
|
|
||||||
|
children_output = {'media_type': helpers.get_xml_attr(m, 'type'),
|
||||||
|
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
|
||||||
|
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
|
||||||
|
'rating_key': helpers.get_xml_attr(m, 'ratingKey'),
|
||||||
|
'parent_rating_key': helpers.get_xml_attr(m, 'parentRatingKey'),
|
||||||
|
'grandparent_rating_key': helpers.get_xml_attr(m, 'grandparentRatingKey'),
|
||||||
|
'title': helpers.get_xml_attr(m, 'title'),
|
||||||
|
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
|
||||||
|
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
|
||||||
|
'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'),
|
||||||
|
'studio': helpers.get_xml_attr(m, 'studio'),
|
||||||
|
'content_rating': helpers.get_xml_attr(m, 'contentRating'),
|
||||||
|
'summary': helpers.get_xml_attr(m, 'summary'),
|
||||||
|
'tagline': helpers.get_xml_attr(m, 'tagline'),
|
||||||
|
'rating': helpers.get_xml_attr(m, 'rating'),
|
||||||
|
'audience_rating': helpers.get_xml_attr(m, 'audienceRating'),
|
||||||
|
'user_rating': helpers.get_xml_attr(m, 'userRating'),
|
||||||
|
'duration': helpers.get_xml_attr(m, 'duration'),
|
||||||
|
'year': helpers.get_xml_attr(m, 'year'),
|
||||||
|
'thumb': helpers.get_xml_attr(m, 'thumb'),
|
||||||
|
'parent_thumb': helpers.get_xml_attr(m, 'parentThumb'),
|
||||||
|
'grandparent_thumb': helpers.get_xml_attr(m, 'grandparentThumb'),
|
||||||
|
'art': helpers.get_xml_attr(m, 'art'),
|
||||||
|
'banner': helpers.get_xml_attr(m, 'banner'),
|
||||||
|
'originally_available_at': helpers.get_xml_attr(m, 'originallyAvailableAt'),
|
||||||
|
'added_at': helpers.get_xml_attr(m, 'addedAt'),
|
||||||
|
'updated_at': helpers.get_xml_attr(m, 'updatedAt'),
|
||||||
|
'last_viewed_at': helpers.get_xml_attr(m, 'lastViewedAt'),
|
||||||
|
'guid': helpers.get_xml_attr(m, 'guid'),
|
||||||
|
'directors': directors,
|
||||||
|
'writers': writers,
|
||||||
|
'actors': actors,
|
||||||
|
'genres': genres,
|
||||||
|
'labels': labels,
|
||||||
|
'full_title': helpers.get_xml_attr(m, 'title')
|
||||||
|
}
|
||||||
children_list.append(children_output)
|
children_list.append(children_output)
|
||||||
|
|
||||||
output = {'children_count': helpers.get_xml_attr(xml_head[0], 'size'),
|
output = {'children_count': helpers.get_xml_attr(xml_head[0], 'size'),
|
||||||
|
@ -2157,7 +2248,7 @@ class PmsConnect(object):
|
||||||
if str(section_id).isdigit():
|
if str(section_id).isdigit():
|
||||||
library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key, output_format='xml')
|
library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key, output_format='xml')
|
||||||
elif str(rating_key).isdigit():
|
elif str(rating_key).isdigit():
|
||||||
library_data = self.get_children_list(str(rating_key), output_format='xml')
|
library_data = self.get_metadata_children(str(rating_key), output_format='xml')
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
|
logger.warn(u"Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
|
||||||
return []
|
return []
|
||||||
|
|
|
@ -39,6 +39,7 @@ import http_handler
|
||||||
import libraries
|
import libraries
|
||||||
import log_reader
|
import log_reader
|
||||||
import logger
|
import logger
|
||||||
|
import newsletters
|
||||||
import mobile_app
|
import mobile_app
|
||||||
import notification_handler
|
import notification_handler
|
||||||
import notifiers
|
import notifiers
|
||||||
|
@ -5285,3 +5286,238 @@ class WebInterface(object):
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
def get_plexpy_url(self, **kwargs):
|
def get_plexpy_url(self, **kwargs):
|
||||||
return helpers.get_plexpy_url()
|
return helpers.get_plexpy_url()
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@requireAuth()
|
||||||
|
def newsletter(self, **kwargs):
|
||||||
|
news_letter = newsletters.Newsletter()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
|
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
return serve_template(templatename="newsletter_template.html",
|
||||||
|
title="Newsletter",
|
||||||
|
recently_added=news_letter.recently_added,
|
||||||
|
start_date=news_letter.start_date,
|
||||||
|
end_date=news_letter.end_date,
|
||||||
|
config=config)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth()
|
||||||
|
def newsletter_raw(self, **kwargs):
|
||||||
|
news_letter = newsletters.Newsletter()
|
||||||
|
|
||||||
|
if news_letter.recently_added:
|
||||||
|
return news_letter.recently_added
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def get_newsletters(self, **kwargs):
|
||||||
|
""" Get a list of configured newsletters.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json:
|
||||||
|
[{"id": 1,
|
||||||
|
"agent_id": 13,
|
||||||
|
"agent_name": "recently_added",
|
||||||
|
"agent_label": "Recently Added",
|
||||||
|
"friendly_name": "",
|
||||||
|
"cron": "0 0 * * 1",
|
||||||
|
"active": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
result = newsletters.get_newsletters()
|
||||||
|
return result
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
def get_newsletters_table(self, **kwargs):
|
||||||
|
result = newsletters.get_newsletters()
|
||||||
|
return serve_template(templatename="newsletters_table.html", newsletters_list=result)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def delete_newsletter(self, newsletter_id=None, **kwargs):
|
||||||
|
""" Remove a newsletter from the database.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
newsletter_id (int): The newsletter to delete
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
result = newsletters.delete_newsletter(newsletter_id=newsletter_id)
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Newsletter deleted successfully.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Failed to delete newsletter.'}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def get_newsletter_config(self, newsletter_id=None, **kwargs):
|
||||||
|
""" Get the configuration for an existing notification agent.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
newsletter_id (int): The newsletter config to retrieve
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json:
|
||||||
|
{"id": 1,
|
||||||
|
"agent_id": 13,
|
||||||
|
"agent_name": "recently_added",
|
||||||
|
"agent_label": "Recently Added",
|
||||||
|
"friendly_name": "",
|
||||||
|
"cron": "0 0 * * 1",
|
||||||
|
"active": 1
|
||||||
|
"config": {"last_days": 7,
|
||||||
|
"incl_movies": 1,
|
||||||
|
"incl_shows": 1,
|
||||||
|
"incl_artists": 1,
|
||||||
|
},
|
||||||
|
"config_options": [{...}, ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
def get_newsletter_config_modal(self, newsletter_id=None, **kwargs):
|
||||||
|
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||||
|
return serve_template(templatename="newsletter_config.html", newsletter=result)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def add_newsletter_config(self, agent_id=None, **kwargs):
|
||||||
|
""" Add a new notification agent.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
agent_id (int): The newsletter type to add
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
result = newsletters.add_newsletter_config(agent_id=agent_id, **kwargs)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Added newsletter.', 'newsletter_id': result}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Failed to add newsletter.'}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
|
||||||
|
""" Configure an exisitng notificaiton agent.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
newsletter_id (int): The newsletter config to update
|
||||||
|
agent_id (int): The newsletter type of the newsletter
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
Pass all the config options for the agent with the agent prefix:
|
||||||
|
e.g. For Recently Added: recently_added_last_days
|
||||||
|
recently_added_incl_movies
|
||||||
|
recently_added_incl_shows
|
||||||
|
recently_added_incl_artists
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
result = newsletters.set_newsletter_config(newsletter_id=newsletter_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Saved newsletter.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Failed to save newsletter.'}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi("notify")
|
||||||
|
def send_newsletter(self, newsletter_id=None, test=False, **kwargs):
|
||||||
|
""" Send a newsletter using Tautulli.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
newsletter_id (int): The ID number of the newsletter
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||||
|
|
||||||
|
test = 'test ' if test else ''
|
||||||
|
|
||||||
|
if newsletter_id:
|
||||||
|
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||||
|
|
||||||
|
if newsletter:
|
||||||
|
logger.debug(u"Sending %s%s newsletter." % (test, newsletter['agent_name']))
|
||||||
|
if newsletter_handler.send(newsletter_id=newsletter_id,
|
||||||
|
**kwargs):
|
||||||
|
return "Newsletter sent."
|
||||||
|
else:
|
||||||
|
return "Newsletter failed."
|
||||||
|
else:
|
||||||
|
logger.debug(u"Unable to send %snewsletter, invalid newsletter_id %s." % (test, newsletter_id))
|
||||||
|
return "Invalid newsletter id %s." % newsletter_id
|
||||||
|
else:
|
||||||
|
logger.debug(u"Unable to send %snotification, no newsletter_id received." % test)
|
||||||
|
return "No newsletter id received."
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
def preview_newsletter(self, newsletter_id=None, **kwargs):
|
||||||
|
if newsletter_id:
|
||||||
|
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
|
||||||
|
newsletter_agent = newsletters.get_agent_class(agent_id=newsletter['agent_id'], config=newsletter['config'])
|
||||||
|
if newsletter_agent:
|
||||||
|
return newsletter_agent.preview()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue