Initial newsletter support

This commit is contained in:
JonnyWong16 2018-01-06 22:27:49 -08:00
parent b73d2ff1f7
commit 0f39201774
15 changed files with 2454 additions and 123 deletions

View file

@ -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%;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

View 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 &nbsp;<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>&nbsp; ' + xhr.responseText;
showMsg(msg, false, true, 2000);
} else {
var msg = '<i class="fa fa-times"></i>&nbsp; ' + 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

View 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']} &nbsp;<span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
% else:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']})</span>
% endif
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span>
</span>
</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>

View file

@ -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>&nbsp; ' + xhr.responseText; var msg = '<i class="fa fa-check"></i>&nbsp; ' + xhr.responseText;
showMsg(msg, false, true, 2000); showMsg(msg, false, true, 2000);
} else { } else {
msg = '<i class="fa fa-times"></i>&nbsp; ' + xhr.responseText; var msg = '<i class="fa fa-times"></i>&nbsp; ' + xhr.responseText;
showMsg(msg, false, true, 2000, true); showMsg(msg, false, true, 2000, true);
} }
} }

View file

@ -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));

View 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">&#9734;&#9734;&#9734;&#9734;&#9734;</span>
<span class="star-rating-full" style="width: ${float(movie['rating'])/0.1}%">&#9733;&#9733;&#9733;&#9733;&#9733;</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">&#9734;&#9734;&#9734;&#9734;&#9734;</span>
<span class="star-rating-full" style="width: ${float(show['rating'])/0.1}%">&#9733;&#9733;&#9733;&#9733;&#9733;</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">&#9734;&#9734;&#9734;&#9734;&#9734;</span>
<span class="star-rating-full" style="width: ${float(album['rating'])/0.1}%">&#9733;&#9733;&#9733;&#9733;&#9733;</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

View file

@ -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, '

View file

@ -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
View 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

View file

@ -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:

View file

@ -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):

View file

@ -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 []

View file

@ -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