Compare commits

..

42 Commits

Author SHA1 Message Date
JonnyWong16
e91ba46265 v2.1.6-beta 2018-05-09 20:26:06 -07:00
JonnyWong16
62104c95e3 Move newsletter filename setting and rename config tab 2018-05-08 19:39:27 -07:00
JonnyWong16
178bd89e7c Better init config checkboxes JS 2018-05-08 17:40:17 -07:00
JonnyWong16
365260401c Hide newsletter agent options when save only checked 2018-05-08 17:25:25 -07:00
JonnyWong16
04029bd4d3 Fix NoneType error in newsletter ID name for parameters 2018-05-08 12:56:12 -07:00
JonnyWong16
9cf1128712 Update newsletter preview note for image hosting 2018-05-08 11:22:16 -07:00
JonnyWong16
2eebce9f6c Add HTTP root to newsletter ID name note 2018-05-08 11:01:20 -07:00
JonnyWong16
b08e071b81 Note newesletter ID name and filename as optional 2018-05-08 10:59:00 -07:00
JonnyWong16
7778d84728 Add setting to specify newsletter ID name for static URLs 2018-05-08 10:54:07 -07:00
JonnyWong16
8e3fe7bfa2 v2.1.5-beta 2018-05-07 21:22:46 -07:00
JonnyWong16
6f22c823be Typo in static newsletter URL help text 2018-05-07 21:03:09 -07:00
JonnyWong16
34d7c67813 Merge pull request #1287 from RafaelSchridi/patch-3
Typo in Newsletter Parameters
2018-05-07 21:02:25 -07:00
JonnyWong16
862ed5ce9f Add option to save newsletter only 2018-05-07 20:49:37 -07:00
JonnyWong16
84406e6797 Add setting to enable static newsletter URL 2018-05-07 20:39:04 -07:00
JonnyWong16
19cf567366 Add option to change the newsletter filename 2018-05-07 20:02:04 -07:00
JonnyWong16
8af697a157 Add URL to retrieve latest scheduled newsletter 2018-05-07 19:42:04 -07:00
JonnyWong16
76122bea5d Fix Imgur URL database upgrade migration 2018-05-07 19:09:35 -07:00
JonnyWong16
1a12422908 Show local note on newsletter preview if image hosting enabled 2018-05-07 18:26:40 -07:00
JonnyWong16
2df9f0b48b Add Cloudinary versioning when retrieving the image 2018-05-07 16:13:00 -07:00
JonnyWong16
8540b80e57 Refresh image when uploading to image hosting 2018-05-07 15:32:21 -07:00
JonnyWong16
8ad565a444 Add visible Plex server identifier in the settings 2018-05-07 09:13:16 -07:00
JonnyWong16
f91b6481b3 Close email SMTP connection cleanly if exception 2018-05-06 22:43:55 -07:00
JonnyWong16
826db082c9 Add setting for custom newsletter templates folder 2018-05-06 15:37:07 -07:00
Rafael Schridi
f3d64a7886 Typo in Newsletter Parameters 2018-05-06 23:05:36 +02:00
JonnyWong16
031d078bc2 Add Cache-Control header for newsletter images (Fixes Tautulli/Tautulli-Issues#71) 2018-05-06 08:34:21 -07:00
JonnyWong16
04fcd78102 Uncoulpe self-hosted newsletter and image setting 2018-05-06 08:33:28 -07:00
JonnyWong16
53d1e0f541 Remove HTTP root from newsletter preview iframe 2018-05-05 23:40:44 -07:00
JonnyWong16
9719f0b25b Update newsletter warning message 2018-05-05 22:59:10 -07:00
JonnyWong16
6d1d5bc822 Check for disabled image hosting 2018-05-05 13:06:20 -07:00
JonnyWong16
0d7bbe044d Don't fetch recently added on homepage if Plex Cloud server is sleeping 2018-05-05 11:54:13 -07:00
JonnyWong16
b1dc5816a4 v2.1.4 2018-05-05 11:21:03 -07:00
JonnyWong16
476011a783 Fix newsletter URL with no HTTP root 2018-05-05 11:13:27 -07:00
JonnyWong16
e038c57c4c v2.1.3-beta 2018-05-04 22:36:11 -07:00
JonnyWong16
a989a53750 Encode image title for Cloudinary upload 2018-05-04 16:11:42 -07:00
JonnyWong16
d8cfdea704 Log individual condition evalutation 2018-05-04 15:52:04 -07:00
JonnyWong16
ed4722c4ce Improve refreshing of cached Plex images 2018-05-03 20:29:23 -07:00
JonnyWong16
17ab5f05ed Patch apshceduler sun-sat as 0-6 2018-05-03 17:58:28 -07:00
JonnyWong16
71ab2248d7 Make sure Cloudinary parameters are strings 2018-05-03 08:34:32 -07:00
JonnyWong16
4fb4410552 Fix potential XSS in search 2018-05-02 10:26:05 -07:00
JonnyWong16
a915d2333f Catch failed hostname resolution (Fixes Tautulli/Tautulli-Issues#68) 2018-05-01 16:57:43 -07:00
JonnyWong16
aaf5a18251 Forgot missing '/' 2018-05-01 15:51:23 -07:00
JonnyWong16
b90026801b Fix double HTTP root in newsletter URL 2018-05-01 15:37:37 -07:00
24 changed files with 498 additions and 208 deletions

View File

@@ -1,5 +1,47 @@
# Changelog # Changelog
## v2.1.6-beta (2018-05-09)
* Newsletters:
* Change: Setting to specify static URL ID name instead of using the newsletter ID number.
* Change: Reorganize newsletter config options.
## v2.1.5-beta (2018-05-07)
* Newsletters:
* New: Added setting for a custom newsletter template folder.
* New: Added option to enable static newsletter URLs to retrieve the last sent scheduled newsletter.
* New: Added ability to change the newsletter output directory and filenames.
* New: Added option to save the newsletter file without sending it to a notification agent.
* Fix: Check for disabled image hosting setting.
* Fix: Cache newsletter images when refreshing the page.
* Fix: Refresh image from the Plex server when uploading to image hosting.
* Change: Allow all image hosting options with self-hosted newsletters.
* UI:
* Change: Don't retrieve recently added on the homepage if the Plex Cloud server is sleeping.
* Other:
* Fix: Imgur database upgrade migration.
## v2.1.4 (2018-05-05)
* Newsletters:
* Fix: Newsletter URL without an HTTP root.
## v2.1.3-beta (2018-05-04)
* Newsletters:
* Fix: HTTP root doubled in newsletter URL.
* Fix: Configuration would not open with failed hostname resolution.
* Fix: Schedule one day off when using weekday names in cron.
* Fix: Images not refreshing when changed in Plex.
* Fix: Cloudinary upload with non-ASCII image titles.
* Other:
* Fix: Potential XSS vulnerability in search.
## v2.1.2-beta (2018-05-01) ## v2.1.2-beta (2018-05-01)
* Newsletters: * Newsletters:

View File

@@ -4093,3 +4093,8 @@ a:hover .overlay-refresh-image:hover {
a[data-tab-destination] { a[data-tab-destination] {
cursor: pointer; cursor: pointer;
} }
.modal-config-section {
margin-top: 10px !important;
padding-top: 10px;
border-top: 1px solid #444;
}

View File

@@ -5,6 +5,7 @@
</%def> </%def>
<%def name="body()"> <%def name="body()">
<% from plexpy import PLEX_SERVER_UP %>
<div class="container-fluid"> <div class="container-fluid">
% for section in config['home_sections']: % for section in config['home_sections']:
% if section == 'current_activity': % if section == 'current_activity':
@@ -22,7 +23,6 @@
</h3> </h3>
</div> </div>
<div id="currentActivity"> <div id="currentActivity">
<% from plexpy import PLEX_SERVER_UP %>
% if PLEX_SERVER_UP: % if PLEX_SERVER_UP:
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div> <div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
% elif config['pms_is_cloud']: % elif config['pms_is_cloud']:
@@ -135,7 +135,17 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div id="recentlyAdded" style="margin-right: -15px;"> <div id="recentlyAdded" style="margin-right: -15px;">
% if PLEX_SERVER_UP:
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div> <div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
% elif config['pms_is_cloud']:
<div class="text-muted">Plex Cloud server is sleeping.</div>
% else:
<div class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin':
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
% endif
</div>
% endif
<br> <br>
</div> </div>
</div> </div>
@@ -222,6 +232,7 @@
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<% from plexpy import PLEX_SERVER_UP %>
<script src="${http_root}js/moment-with-locale.js"></script> <script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.scrollbar.min.js"></script> <script src="${http_root}js/jquery.scrollbar.min.js"></script>
<script src="${http_root}js/jquery.mousewheel.min.js"></script> <script src="${http_root}js/jquery.mousewheel.min.js"></script>
@@ -254,7 +265,6 @@
}); });
} }
</script> </script>
<% from plexpy import PLEX_SERVER_UP %>
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP: % if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
<script> <script>
var defaultHandler = { var defaultHandler = {
@@ -746,7 +756,7 @@
getLibraryStats(); getLibraryStats();
</script> </script>
% endif % endif
% if 'recently_added' in config['home_sections']: % if 'recently_added' in config['home_sections'] and PLEX_SERVER_UP:
<script> <script>
function recentlyAdded(recently_added_count, recently_added_type) { function recentlyAdded(recently_added_count, recently_added_type) {
showMsg("Loading recently added items...", true, false, 0); showMsg("Loading recently added items...", true, false, 0);

View File

@@ -1,17 +1,17 @@
function initConfigCheckbox(elem) { function initConfigCheckbox(elem, toggleElem = null, reverse = false) {
var config = $(elem).closest('div').next(); var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
config.css('overflow', 'hidden'); config.css('overflow', 'hidden');
if ($(elem).is(":checked")) { if ($(elem).is(":checked")) {
config.show(); config.toggle(!reverse);
} else { } else {
config.hide(); config.toggle(reverse);
} }
$(elem).click(function () { $(elem).click(function () {
var config = $(this).closest('div').next(); var config = toggleElem ? $(toggleElem) : $(this).closest('div').next();
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
config.slideDown(); config.slideToggleBool(!reverse);
} else { } else {
config.slideUp(); config.slideToggleBool(reverse);
} }
}); });
} }
@@ -450,3 +450,7 @@ function forceMinMax(elem) {
function capitalizeFirstLetter(string) { function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
$.fn.slideToggleBool = function(bool, options) {
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
}

View File

@@ -20,7 +20,7 @@
<div class="row"> <div class="row">
<ul class="nav nav-tabs list-unstyled" role="tablist"> <ul class="nav nav-tabs list-unstyled" role="tablist">
<li role="presentation" class="active"><a href="#tabs-newsletter_config" aria-controls="tabs-newsletter_config" role="tab" data-toggle="tab">Configuration</a></li> <li role="presentation" class="active"><a href="#tabs-newsletter_config" aria-controls="tabs-newsletter_config" role="tab" data-toggle="tab">Configuration</a></li>
<li role="presentation"><a href="#tabs-newsletter_agent" aria-controls="tabs-newsletter_agent" role="tab" data-toggle="tab">Notification Agent</a></li> <li role="presentation"><a href="#tabs-newsletter_saving_sending" aria-controls="tabs-newsletter_saving_sending" role="tab" data-toggle="tab">Saving & Sending</a></li>
<li role="presentation"><a href="#tabs-newsletter_text" aria-controls="tabs-newsletter_text" role="tab" data-toggle="tab">Newsletter Text</a></li> <li role="presentation"><a href="#tabs-newsletter_text" aria-controls="tabs-newsletter_text" role="tab" data-toggle="tab">Newsletter Text</a></li>
<li role="presentation"><a href="#tabs-test_newsletter" aria-controls="tabs-test_newsletter" role="tab" data-toggle="tab">Test Newsletter</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> </ul>
@@ -70,7 +70,7 @@
<p class="help-block">Set the time frame to include in the newsletter. Note: Days uses calendar days (i.e. since midnight).</p> <p class="help-block">Set the time frame to include in the newsletter. Note: Days uses calendar days (i.e. since midnight).</p>
</div> </div>
</div> </div>
<div class="col-md-12" style="padding-top: 10px; border-top: 1px solid #444;"> <div class="col-md-12 modal-config-section">
<input type="hidden" id="newsletter_id" name="newsletter_id" value="${newsletter['id']}" /> <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']}" /> <input type="hidden" id="agent_id" name="agent_id" value="${newsletter['agent_id']}" />
% for item in newsletter['config_options']: % for item in newsletter['config_options']:
@@ -165,7 +165,16 @@
% endif % endif
% endfor % endfor
</div> </div>
<div class="col-md-12" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #444;"> <div class="col-md-12 modal-config-section">
<div class="form-group">
<label for="id_name">Unique ID Name</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="id_name" name="id_name" value="${newsletter['id_name']}" size="30">
</div>
</div>
<p class="help-block">Optional: Enter a unique ID name to create a static URL to the last sent scheduled newsletter at <span class="inline-pre">${http_root}newsletter/id/&lt;id_name&gt;</span>. Only letters (a-z), numbers (0-9), underscores (_) and hyphens (-) are allowed. Leave blank to disable.</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="friendly_name">Description</label> <label for="friendly_name">Description</label>
<div class="row"> <div class="row">
@@ -178,12 +187,32 @@
</div> </div>
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_agent"> <div role="tabpanel" class="tab-pane" id="tabs-newsletter_saving_sending">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label>Saving</label>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="newsletter_config_formatted_checkbox" data-id="newsletter_config_formatted" class="checkboxes" value="1" ${checked(newsletter['config']['formatted'])}> Send newsletter as an HTML formatted Email <input type="checkbox" id="newsletter_config_save_only_checkbox" data-id="newsletter_config_save_only" class="checkboxes" value="1" ${checked(newsletter['config']['save_only'])}> Save HTML File Only
</label>
<p class="help-block">Enable to save the newsletter HTML file without sending it to any notification agent.</p>
<input type="hidden" id="newsletter_config_save_only" name="newsletter_config_save_only" value="${newsletter['config']['save_only']}">
</div>
<div class="form-group">
<label for="newsletter_config_filename">HTML File Name</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="newsletter_config_filename" name="newsletter_config_filename" value="${newsletter['config']['filename']}" size="30">
</div>
</div>
<p class="help-block">Optional: Enter the file name to use when saving the newsletter (ending with <span class="inline-pre">.html</span>). You may use any of the <a href="#newsletter-text-sub-modal" data-toggle="modal">newsletter text parameters</a>. Leave blank for default.</p>
</div>
</div>
<div class="col-md-12 modal-config-section" id="newsletter_agent_options">
<label>Sending</label>
<div class="checkbox">
<label>
<input type="checkbox" id="newsletter_config_formatted_checkbox" data-id="newsletter_config_formatted" class="checkboxes" value="1" ${checked(newsletter['config']['formatted'])}> Send Newsletter as an HTML Formatted Email
</label> </label>
<p class="help-block">Enable to send the newsletter as an HTML formatted Email. Disable to only send a subject and body message to a different notification agent.</p> <p class="help-block">Enable to send the newsletter as an HTML formatted Email. Disable to only send a subject and body message to a different notification agent.</p>
<input type="hidden" id="newsletter_config_formatted" name="newsletter_config_formatted" value="${newsletter['config']['formatted']}"> <input type="hidden" id="newsletter_config_formatted" name="newsletter_config_formatted" value="${newsletter['config']['formatted']}">
@@ -234,100 +263,100 @@
Note: Self-hosted newsletters must be enabled under <a data-tab-destination="tabs-notifications" data-dismiss="modal" data-target="#newsletter_self_hosted">Newsletters</a> to include a link to the newsletter. Note: Self-hosted newsletters must be enabled under <a data-tab-destination="tabs-notifications" data-dismiss="modal" data-target="#newsletter_self_hosted">Newsletters</a> to include a link to the newsletter.
</p> </p>
</div> </div>
</div> <div id="newsletter-email-config">
<div id="newsletter-email-config" class="col-md-12" style="padding-top: 10px; border-top: 1px solid #444;"> % for item in newsletter['email_config_options']:
% for item in newsletter['email_config_options']: % if item['input_type'] == 'help':
% if item['input_type'] == 'help': <div class="form-group">
<div class="form-group"> <label>${item['label']}</label>
<label>${item['label']}</label> <p class="help-block">${item['description'] | n}</p>
<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-12">
<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> </div>
<p class="help-block">${item['description'] | n}</p> % elif item['input_type'] == 'text' or item['input_type'] == 'password':
</div> <div class="form-group">
% elif item['input_type'] == 'number': <label for="${item['name']}">${item['label']}</label>
<div class="form-group"> <div class="row">
<label for="${item['name']}">${item['label']}</label> <div class="col-md-12">
<div class="row"> <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 class="col-md-3"> </div>
<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> </div>
<p class="help-block">${item['description'] | n}</p> % elif item['input_type'] == 'number':
</div> <div class="form-group">
% elif item['input_type'] == 'button': <label for="${item['name']}">${item['label']}</label>
<div class="form-group"> <div class="row">
<label for="${item['name']}">${item['label']}</label> <div class="col-md-3">
<div class="row"> <input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
<div class="col-md-12"> </div>
<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> </div>
<p class="help-block">${item['description'] | n}</p> % elif item['input_type'] == 'button':
</div> <div class="form-group">
% elif item['input_type'] == 'checkbox' and item['name'] != 'newsletter_email_html_support': <label for="${item['name']}">${item['label']}</label>
<div class="checkbox"> <div class="row">
<label> <div class="col-md-12">
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']} <input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</label> </div>
<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-12">
<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> </div>
<p class="help-block">${item['description'] | n}</p> % elif item['input_type'] == 'checkbox' and item['name'] != 'newsletter_email_html_support':
</div> <div class="checkbox">
% elif item['input_type'] == 'selectize': <label>
<div class="form-group"> <input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
<label for="${item['name']}">${item['label']}</label> </label>
<div class="row"> <p class="help-block">${item['description'] | n}</p>
<div class="col-md-12"> <input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
<select class="form-control" id="${item['name']}" name="${item['name']}"> </div>
<option value="select-all">Select All</option> % elif item['input_type'] == 'select':
<option value="remove-all">Remove All</option> <div class="form-group">
% if isinstance(item['select_options'], dict): <label for="${item['name']}">${item['label']}</label>
% for section, options in item['select_options'].iteritems(): <div class="row">
<optgroup label="${section}"> <div class="col-md-12">
% for option in sorted(options, key=lambda x: x['text'].lower()): <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>
% elif item['input_type'] == 'selectize':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
</optgroup>
% endfor
% else:
<option value="border-all"></option>
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option> <option value="${option['value']}">${option['text']}</option>
% endfor % endfor
</optgroup> % endif
% endfor </select>
% else: </div>
<option value="border-all"></option>
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
% endif
</select>
</div> </div>
<p class="help-block">${item['description'] | n}</p>
</div> </div>
<p class="help-block">${item['description'] | n}</p> % endif
% endfor
<input type="hidden" id="newsletter_email_html_support" name="newsletter_email_html_support" value="1">
</div> </div>
% endif
% endfor
<input type="hidden" id="newsletter_email_html_support" name="newsletter_email_html_support" value="1">
</div> </div>
</div> </div>
</div> </div>
@@ -458,6 +487,26 @@
toggleCustomCron(); toggleCustomCron();
}); });
function validateFilename() {
var filename = $('#newsletter_config_filename').val();
if (filename !== '' && !(filename.endsWith('.html'))) {
showMsg('<i class="fa fa-times"></i> Failed to save newsletter. Invalid file name.', false, true, 5000, true);
return false;
} else {
return true;
}
}
function validateIDName() {
var id_name = $('#id_name').val();
if (/^[a-zA-Z0-9_-]*$/.test(id_name)) {
return true;
} else {
showMsg('<i class="fa fa-times"></i> Failed to save newsletter. Invalid unique ID name.', false, true, 5000, true);
return false;
}
}
var $incl_libraries = $('#newsletter_config_incl_libraries').selectize({ var $incl_libraries = $('#newsletter_config_incl_libraries').selectize({
plugins: ['remove_button'], plugins: ['remove_button'],
maxItems: null, maxItems: null,
@@ -485,6 +534,8 @@
var incl_libraries = $incl_libraries[0].selectize; var incl_libraries = $incl_libraries[0].selectize;
incl_libraries.setValue(${json.dumps(next((c['value'] for c in newsletter['config_options'] if c['name'] == 'newsletter_config_incl_libraries'), [])) | n}); incl_libraries.setValue(${json.dumps(next((c['value'] for c in newsletter['config_options'] if c['name'] == 'newsletter_config_incl_libraries'), [])) | n});
initConfigCheckbox('#newsletter_config_save_only_checkbox', '#newsletter_agent_options', true);
function toggleEmailSelect () { function toggleEmailSelect () {
if ($('#newsletter_config_formatted_checkbox').is(':checked')) { if ($('#newsletter_config_formatted_checkbox').is(':checked')) {
$('#newsletter_body').hide(); $('#newsletter_body').hide();
@@ -643,7 +694,9 @@
if ($('#custom_cron').val() === '0'){ if ($('#custom_cron').val() === '0'){
$("#cron_value").val(cron_widget.cron('value')); $("#cron_value").val(cron_widget.cron('value'));
} }
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, true, saveCallback); if (validateFilename() && validateIDName()){
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, true, saveCallback);
}
} }
$('#delete-newsletter-item').click(function () { $('#delete-newsletter-item').click(function () {

View File

@@ -32,7 +32,7 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
var frame = $('<iframe></iframe>', { var frame = $('<iframe></iframe>', {
src: '${http_root}real_newsletter?${urllib.urlencode(kwargs) | n}', src: 'real_newsletter?${urllib.urlencode(kwargs) | n}',
frameborder: '0', frameborder: '0',
style: 'display: none; height: 100vh; width: 100vw;' style: 'display: none; height: 100vh; width: 100vw;'
}); });

View File

@@ -123,7 +123,7 @@
% endif % endif
% endfor % endfor
</div> </div>
<div class="col-md-12" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #444;"> <div class="col-md-12 modal-config-section">
<div class="form-group"> <div class="form-group">
<label for="friendly_name">Description</label> <label for="friendly_name">Description</label>
<div class="row"> <div class="row">

View File

@@ -28,15 +28,17 @@
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<script> <script>
var query_string = "${query.replace('"','\\"').replace('/','\\/') | n}";
$('#search_button').removeClass('btn-inactive'); $('#search_button').removeClass('btn-inactive');
$('#query').val("${query.replace('"','\\"') | n}").css({ right: '0', width: '250px' }).addClass('active'); $('#query').val(query_string).css({ right: '0', width: '250px' }).addClass('active');
$.ajax({ $.ajax({
url: 'get_search_results_children', url: 'get_search_results_children',
type: "GET", type: "POST",
async: true, async: true,
data: { data: {
query: "${query.replace('"','\\"') | n}", query: query_string,
limit: 30 limit: 30
}, },
complete: function (xhr, status) { complete: function (xhr, status) {

View File

@@ -702,6 +702,17 @@
The server URL that Tautulli will use to connect to your Plex server. Retrieved automatically. The server URL that Tautulli will use to connect to your Plex server. Retrieved automatically.
</p> </p>
</div> </div>
<div class="form-group advanced-setting">
<label for="pms_url">Plex Server Identifier</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}" size="30" readonly>
</div>
</div>
<p class="help-block">
The unique identifier for your Plex server. Retrieved automatically.
</p>
</div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
<label> <label>
<input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection <input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
@@ -728,7 +739,6 @@
</div> </div>
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}"> <input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;"> <input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
<div class="form-group advanced-setting"> <div class="form-group advanced-setting">
@@ -956,12 +966,30 @@
</div> </div>
<div id="self_host_newsletter_options" style="overlfow: hidden; display: ${'block' if config['newsletter_self_hosted'] == 'checked' else 'none'}"> <div id="self_host_newsletter_options" style="overlfow: hidden; display: ${'block' if config['newsletter_self_hosted'] == 'checked' else 'none'}">
<p class="help-block" id="self_host_newsletter_message"> <p class="help-block" id="self_host_newsletter_message">
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.<br> Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
Note: Newsletter images will be self-hosted regardless of the Image Hosting setting below.<br>
</p> </p>
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p> <p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
</div> </div>
<div class="form-group advanced-setting">
<label for="newsletter_dir">Custom Newsletter Templates Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
</div>
</div>
<p class="help-block">Optional: Enter the full path to your custom newsletter templates folder. Leave blank for default.</p>
</div>
<div class="form-group advanced-setting">
<label for="newsletter_dir">Newsletter Output Directory</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}">
</div>
</div>
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
</div>
<div class="padded-header"> <div class="padded-header">
<h3>3rd Party APIs</h3> <h3>3rd Party APIs</h3>
</div> </div>
@@ -1084,7 +1112,7 @@
Add a new newsletter agent, or configure an existing newsletter agent by clicking the settings icon on the right. Add a new newsletter agent, or configure an existing newsletter agent by clicking the settings icon on the right.
</p> </p>
<p class="help-block settings-warning" id="newsletter_upload_warning"> <p class="help-block settings-warning" id="newsletter_upload_warning">
Note: Either <a data-tab-destination="tabs-notifications" data-target="#notify_upload_posters">Image Hosting</a> or <a data-tab-destination="tabs-notifications" data-target="#newsletter_self_hosted">Self-Hosted Newsletters</a> must be enabled.</span> Warning: The <a data-tab-destination="tabs-notifications" data-target="#notify_upload_posters">Image Hosting</a> setting must be enabled for images to display on the newsletter.</span>
</p> </p>
<br/> <br/>
<div id="plexpy-newsletters-table"> <div id="plexpy-newsletters-table">
@@ -1173,14 +1201,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label for="newsletter_dir">Newsletter Directory</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}">
</div>
</div>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p> <p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
@@ -2605,10 +2625,10 @@ $(document).ready(function() {
}); });
function newsletterUploadEnabled() { function newsletterUploadEnabled() {
if ($('#notify_upload_posters').val() !== '2' || $('#newsletter_self_hosted').is(':checked')) { if ($('#notify_upload_posters').val() === '0') {
$('#newsletter_upload_warning').hide();
} else {
$('#newsletter_upload_warning').show(); $('#newsletter_upload_warning').show();
} else {
$('#newsletter_upload_warning').hide();
} }
} }
newsletterUploadEnabled(); newsletterUploadEnabled();

View File

@@ -188,7 +188,7 @@ DOCUMENTATION :: END
}, },
complete: function (xhr, status) { complete: function (xhr, status) {
$('#search-results-list').html(xhr.responseText); $('#search-results-list').html(xhr.responseText);
$('#update_query_title').html(query_string) $('#update_query_title').text(query_string)
} }
}); });
} }

View File

@@ -1,17 +1,24 @@
% if data: % if data:
<% <%
import plexpy import plexpy
from plexpy.helpers import grouper from plexpy.helpers import grouper, get_img_service
recently_added = data['recently_added'] recently_added = data['recently_added']
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL: if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/' base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
base_url_image = base_url + 'image/'
elif preview: elif preview:
base_url = 'newsletter/' base_url = 'newsletter/'
base_url_image = base_url + 'image/'
else: else:
base_url = base_url_image = '' base_url = ''
service = get_img_service(include_self=True)
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/image/'
elif service and service != 'self-hosted' and preview:
base_url_image = 'newsletter/image/'
else:
base_url_image = ''
%> %>
<!doctype html> <!doctype html>
<html> <html>
@@ -83,6 +90,14 @@
/* ------------------------------------- /* -------------------------------------
HEADER, FOOTER, MAIN HEADER, FOOTER, MAIN
------------------------------------- */ ------------------------------------- */
.local-preview-note {
text-align: center;
padding-top: 10px;
}
.local-preview-note p {
color: #282A2D;
font-size: 12px;
}
.main { .main {
background: #282A2D; background: #282A2D;
border-radius: 3px; border-radius: 3px;
@@ -608,6 +623,11 @@
</style> </style>
</head> </head>
<body class="" style="font-family: 'Open Sans', Helvetica, Arial, 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%;"> <body class="" style="font-family: 'Open Sans', Helvetica, Arial, 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%;">
% if preview and service:
<div class="local-preview-note" style="text-align: center;padding-top: 10px;"><p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;color: #282A2D;font-size: 12px;">Note: Local preview images only - images will be uploaded to ${service.capitalize()} when the newsletter is sent.</p></div> <!-- IGNORE SAVE -->
% elif preview and not service:
<div class="local-preview-note" style="text-align: center;padding-top: 10px;"><p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-weight: 400;margin: 0;color: #282A2D;font-size: 12px;">Warning: The Image Hosting setting must be enabled for images to display on the newsletter.</p></div> <!-- IGNORE SAVE -->
% endif
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;"> <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%;">
<tr> <tr>
<td class="container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;display: block;max-width: 1042px;padding: 10px;width: 1042px;margin: 0 auto !important;"> <td class="container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;display: block;max-width: 1042px;padding: 10px;width: 1042px;margin: 0 auto !important;">

View File

@@ -1,17 +1,24 @@
% if data: % if data:
<% <%
import plexpy import plexpy
from plexpy.helpers import grouper from plexpy.helpers import grouper, get_img_service
recently_added = data['recently_added'] recently_added = data['recently_added']
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL: if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/' base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
base_url_image = base_url + 'image/'
elif preview: elif preview:
base_url = 'newsletter/' base_url = 'newsletter/'
base_url_image = base_url + 'image/'
else: else:
base_url = base_url_image = '' base_url = ''
service = get_img_service(include_self=True)
if service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
base_url_image = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/image/'
elif service and service != 'self-hosted' and preview:
base_url_image = 'newsletter/image/'
else:
base_url_image = ''
%> %>
<!doctype html> <!doctype html>
<html> <html>
@@ -83,6 +90,14 @@
/* ------------------------------------- /* -------------------------------------
HEADER, FOOTER, MAIN HEADER, FOOTER, MAIN
------------------------------------- */ ------------------------------------- */
.local-preview-note {
text-align: center;
padding-top: 10px;
}
.local-preview-note p {
color: #282A2D;
font-size: 12px;
}
.main { .main {
background: #282A2D; background: #282A2D;
border-radius: 3px; border-radius: 3px;
@@ -608,6 +623,11 @@
</style> </style>
</head> </head>
<body class=""> <body class="">
% if preview and service:
<div class="local-preview-note"><p>Note: Local preview images only - images will be uploaded to ${service.capitalize()} when the newsletter is sent.</p></div> <!-- IGNORE SAVE -->
% elif preview and not service:
<div class="local-preview-note"><p>Warning: The Image Hosting setting must be enabled for images to display on the newsletter.</p></div> <!-- IGNORE SAVE -->
% endif
<table border="0" cellpadding="0" cellspacing="0" class="body"> <table border="0" cellpadding="0" cellspacing="0" class="body">
<tr> <tr>
<td class="container"> <td class="container">

View File

@@ -9,7 +9,7 @@ __all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
'WeekdayPositionExpression', 'LastDayOfMonthExpression') 'WeekdayPositionExpression', 'LastDayOfMonthExpression')
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']

View File

@@ -637,7 +637,7 @@ def dbcheck():
# newsletters table :: This table keeps record of the newsletter settings # newsletters table :: This table keeps record of the newsletter settings
c_db.execute( c_db.execute(
'CREATE TABLE IF NOT EXISTS newsletters (id INTEGER PRIMARY KEY AUTOINCREMENT, ' 'CREATE TABLE IF NOT EXISTS newsletters (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'agent_id INTEGER, agent_name TEXT, agent_label TEXT, ' 'agent_id INTEGER, agent_name TEXT, agent_label TEXT, id_name TEXT NOT NULL DEFAULT "", '
'friendly_name TEXT, newsletter_config TEXT, email_config TEXT, ' 'friendly_name TEXT, newsletter_config TEXT, email_config TEXT, '
'subject TEXT, body TEXT, message TEXT, ' 'subject TEXT, body TEXT, message TEXT, '
'cron TEXT NOT NULL DEFAULT "0 0 * * 0", active INTEGER DEFAULT 0)' 'cron TEXT NOT NULL DEFAULT "0 0 * * 0", active INTEGER DEFAULT 0)'
@@ -648,7 +648,7 @@ def dbcheck():
'CREATE TABLE IF NOT EXISTS newsletter_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, ' 'CREATE TABLE IF NOT EXISTS newsletter_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, '
'newsletter_id INTEGER, agent_id INTEGER, agent_name TEXT, notify_action TEXT, ' 'newsletter_id INTEGER, agent_id INTEGER, agent_name TEXT, notify_action TEXT, '
'subject_text TEXT, body_text TEXT, message_text TEXT, start_date TEXT, end_date TEXT, ' 'subject_text TEXT, body_text TEXT, message_text TEXT, start_date TEXT, end_date TEXT, '
'start_time INTEGER, end_time INTEGER, uuid TEXT UNIQUE, success INTEGER DEFAULT 0)' 'start_time INTEGER, end_time INTEGER, uuid TEXT UNIQUE, filename TEXT, success INTEGER DEFAULT 0)'
) )
# recently_added table :: This table keeps record of recently added items # recently_added table :: This table keeps record of recently added items
@@ -1495,6 +1495,24 @@ def dbcheck():
'ALTER TABLE newsletter_log ADD COLUMN end_time INTEGER' 'ALTER TABLE newsletter_log ADD COLUMN end_time INTEGER'
) )
# Upgrade newsletter_log table from earlier versions
try:
c_db.execute('SELECT filename FROM newsletter_log')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table newsletter_log.")
c_db.execute(
'ALTER TABLE newsletter_log ADD COLUMN filename TEXT'
)
# Upgrade newsletters table from earlier versions
try:
c_db.execute('SELECT id_name FROM newsletters')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table newsletters.")
c_db.execute(
'ALTER TABLE newsletters ADD COLUMN id_name TEXT NOT NULL DEFAULT ""'
)
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id) # Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
try: try:
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone() result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()
@@ -1694,9 +1712,9 @@ def dbcheck():
for row in result: for row in result:
img_hash = notification_handler.set_hash_image_info( img_hash = notification_handler.set_hash_image_info(
rating_key=row['rating_key'], width=1000, height=1500, fallback='poster') rating_key=row['rating_key'], width=1000, height=1500, fallback='poster')
data_factory.set_imgur_info(img_hash=img_hash, imgur_title=row['poster_title'], data_factory.set_img_info(img_hash=img_hash, imgur_title=row['poster_title'],
imgur_url=row['poster_url'], delete_hash=row['delete_hash'], imgur_url=row['poster_url'], delete_hash=row['delete_hash'],
service='imgur') service='imgur')
db.action('DROP TABLE poster_urls') db.action('DROP TABLE poster_urls')
except sqlite3.OperationalError: except sqlite3.OperationalError:

View File

@@ -519,13 +519,16 @@ NEWSLETTER_PARAMETERS = [
'category': 'Global', 'category': 'Global',
'parameters': [ 'parameters': [
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'}, {'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
{'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newesletter.'}, {'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'},
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newesletter.'}, {'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'},
{'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year.'}, {'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year.'},
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'}, {'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'},
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'}, {'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'},
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'}, {'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'},
{'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url', 'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'},
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'}, {'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'},
{'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', 'description': 'The unique ID number for the newsletter agent.'},
{'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', 'description': 'The unique ID name for the newsletter agent.'},
] ]
}, },
{ {

View File

@@ -312,9 +312,11 @@ _CONFIG_DEFINITIONS = {
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0), 'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
'MONITORING_INTERVAL': (int, 'Monitoring', 60), 'MONITORING_INTERVAL': (int, 'Monitoring', 60),
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0), 'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'), 'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),
'NEWSLETTER_DIR': (str, 'Newsletter', ''), 'NEWSLETTER_DIR': (str, 'Newsletter', ''),
'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0), 'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0),
'NEWSLETTER_STATIC_URL': (int, 'Newsletter', 0),
'NMA_APIKEY': (str, 'NMA', ''), 'NMA_APIKEY': (str, 'NMA', ''),
'NMA_ENABLED': (int, 'NMA', 0), 'NMA_ENABLED': (int, 'NMA', 0),
'NMA_PRIORITY': (int, 'NMA', 0), 'NMA_PRIORITY': (int, 'NMA', 0),

View File

@@ -792,8 +792,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
try: try:
response = upload('data:image/png;base64,{}'.format(base64.b64encode(img_data)), response = upload('data:image/png;base64,{}'.format(base64.b64encode(img_data)),
public_id='{}_{}'.format(fallback, rating_key), public_id='{}_{}'.format(fallback, rating_key),
tags=[fallback, rating_key], tags=[fallback, str(rating_key)],
context={'title': img_title, 'rating_key': rating_key, 'fallback': fallback}) context={'title': img_title.encode('utf-8'), 'rating_key': str(rating_key), 'fallback': fallback})
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback)) logger.debug(u"Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback))
img_url = response.get('url', '') img_url = response.get('url', '')
except Exception as e: except Exception as e:
@@ -834,13 +834,14 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
) )
img_options = {} img_options = {'format': img_format,
'version': int(time.time())}
if width != 1000: if width != 1000:
img_options['width'] = width img_options['width'] = str(width)
img_options['crop'] = 'fill' img_options['crop'] = 'fill'
if height != 1500: if height != 1500:
img_options['height'] = height img_options['height'] = str(height)
img_options['crop'] = 'fill' img_options['crop'] = 'fill'
if opacity != 100: if opacity != 100:
img_options['opacity'] = opacity img_options['opacity'] = opacity
@@ -849,14 +850,11 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
if blur != 0: if blur != 0:
img_options['effect'] = 'blur:{}'.format(blur * 100) img_options['effect'] = 'blur:{}'.format(blur * 100)
if img_options: try:
img_options['format'] = img_format url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options)
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback))
try: except Exception as e:
url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options) logger.error(u"Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e))
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback))
except Exception as e:
logger.error(u"Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e))
return url return url
@@ -1072,7 +1070,10 @@ def get_plexpy_url(hostname=None):
s.connect(('<broadcast>', 0)) s.connect(('<broadcast>', 0))
hostname = s.getsockname()[0] hostname = s.getsockname()[0]
except socket.error: except socket.error:
hostname = socket.gethostbyname(socket.gethostname()) try:
hostname = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
pass
if not hostname: if not hostname:
hostname = 'localhost' hostname = 'localhost'

View File

@@ -86,7 +86,9 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
body = newsletter_config['body'] body = newsletter_config['body']
message = newsletter_config['message'] message = newsletter_config['message']
newsletter_agent = newsletters.get_agent_class(agent_id=newsletter_config['agent_id'], newsletter_agent = newsletters.get_agent_class(newsletter_id=newsletter_id,
newsletter_id_name=newsletter_config['id_name'],
agent_id=newsletter_config['agent_id'],
config=newsletter_config['config'], config=newsletter_config['config'],
email_config=newsletter_config['email_config'], email_config=newsletter_config['email_config'],
subject=subject, subject=subject,
@@ -100,6 +102,7 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
subject=newsletter_agent.subject_formatted, subject=newsletter_agent.subject_formatted,
body=newsletter_agent.body_formatted, body=newsletter_agent.body_formatted,
message=newsletter_agent.message_formatted, message=newsletter_agent.message_formatted,
filename=newsletter_agent.filename_formatted,
start_date=newsletter_agent.start_date.format('YYYY-MM-DD'), start_date=newsletter_agent.start_date.format('YYYY-MM-DD'),
end_date=newsletter_agent.end_date.format('YYYY-MM-DD'), end_date=newsletter_agent.end_date.format('YYYY-MM-DD'),
start_time=newsletter_agent.start_time, start_time=newsletter_agent.start_time,
@@ -114,7 +117,7 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
return True return True
def set_notify_state(newsletter, notify_action, subject, body, message, def set_notify_state(newsletter, notify_action, subject, body, message, filename,
start_date, end_date, start_time, end_time, newsletter_uuid): start_date, end_date, start_time, end_time, newsletter_uuid):
if newsletter and notify_action: if newsletter and notify_action:
@@ -133,7 +136,8 @@ def set_notify_state(newsletter, notify_action, subject, body, message,
'start_date': start_date, 'start_date': start_date,
'end_date': end_date, 'end_date': end_date,
'start_time': start_time, 'start_time': start_time,
'end_time': end_time} 'end_time': end_time,
'filename': filename}
db.upsert(table_name='newsletter_log', key_dict=keys, value_dict=values) db.upsert(table_name='newsletter_log', key_dict=keys, value_dict=values)
return db.last_insert_id() return db.last_insert_id()
@@ -149,20 +153,29 @@ def set_notify_success(newsletter_log_id):
db.upsert(table_name='newsletter_log', key_dict=keys, value_dict=values) db.upsert(table_name='newsletter_log', key_dict=keys, value_dict=values)
def get_newsletter(newsletter_uuid): def get_newsletter(newsletter_uuid=None, newsletter_id_name=None):
db = database.MonitorDatabase() db = database.MonitorDatabase()
result = db.select_single('SELECT newsletter_id, start_date, end_date FROM newsletter_log '
'WHERE uuid = ?', [newsletter_uuid]) if newsletter_uuid:
result = db.select_single('SELECT start_date, end_date, uuid, filename FROM newsletter_log '
'WHERE uuid = ?', [newsletter_uuid])
elif newsletter_id_name:
result = db.select_single('SELECT start_date, end_date, uuid, filename FROM newsletter_log '
'JOIN newsletters ON newsletters.id = newsletter_log.newsletter_id '
'WHERE id_name = ? AND notify_action != "test" '
'ORDER BY timestamp DESC LIMIT 1', [newsletter_id_name])
else:
result = None
if result: if result:
newsletter_id = result['newsletter_id'] newsletter_uuid = result['uuid']
start_date = result['start_date'] start_date = result['start_date']
end_date = result['end_date'] end_date = result['end_date']
newsletter_file = result['filename'] or 'newsletter_%s-%s_%s.html' % (start_date.replace('-', ''),
end_date.replace('-', ''),
newsletter_uuid)
newsletter_file = 'newsletter_%s-%s_%s.html' % (start_date.replace('-', ''), newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR or os.path.join(plexpy.DATA_DIR, 'newsletters')
end_date.replace('-', ''),
newsletter_uuid)
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file) newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
if newsletter_file in os.listdir(newsletter_folder): if newsletter_file in os.listdir(newsletter_folder):
@@ -173,4 +186,4 @@ def get_newsletter(newsletter_uuid):
except OSError as e: except OSError as e:
logger.error(u"Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e)) logger.error(u"Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e))
else: else:
logger.warn(u"Tautulli NewsletterHandler :: Newsletter '%s' file is missing." % newsletter_uuid) logger.warn(u"Tautulli NewsletterHandler :: Newsletter file '%s' is missing." % newsletter_file)

View File

@@ -63,12 +63,14 @@ def available_notification_actions():
return actions return actions
def get_agent_class(agent_id=None, config=None, email_config=None, start_date=None, end_date=None, def get_agent_class(newsletter_id=None, newsletter_id_name=None, agent_id=None, config=None, email_config=None,
subject=None, body=None, message=None): start_date=None, end_date=None, subject=None, body=None, message=None):
if str(agent_id).isdigit(): if str(agent_id).isdigit():
agent_id = int(agent_id) agent_id = int(agent_id)
kwargs = {'config': config, kwargs = {'newsletter_id': newsletter_id,
'newsletter_id_name': newsletter_id_name,
'config': config,
'email_config': email_config, 'email_config': email_config,
'start_date': start_date, 'start_date': start_date,
'end_date': end_date, 'end_date': end_date,
@@ -138,7 +140,9 @@ def get_newsletter_config(newsletter_id=None):
subject = result.pop('subject') subject = result.pop('subject')
body = result.pop('body') body = result.pop('body')
message = result.pop('message') message = result.pop('message')
newsletter_agent = get_agent_class(agent_id=result['agent_id'], config=config, email_config=email_config, newsletter_agent = get_agent_class(newsletter_id=newsletter_id, newsletter_id_name=result['id_name'],
agent_id=result['agent_id'],
config=config, email_config=email_config,
subject=subject, body=body, message=message) subject=subject, body=body, message=message)
except Exception as e: except Exception as e:
logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e) logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e)
@@ -176,6 +180,7 @@ def add_newsletter_config(agent_id=None, **kwargs):
values = {'agent_id': agent['id'], values = {'agent_id': agent['id'],
'agent_name': agent['name'], 'agent_name': agent['name'],
'agent_label': agent['label'], 'agent_label': agent['label'],
'id_name': '',
'friendly_name': '', 'friendly_name': '',
'newsletter_config': json.dumps(agent_class.config), 'newsletter_config': json.dumps(agent_class.config),
'email_config': json.dumps(agent_class.email_config), 'email_config': json.dumps(agent_class.email_config),
@@ -223,13 +228,15 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
body = kwargs.pop('body') body = kwargs.pop('body')
message = kwargs.pop('message') message = kwargs.pop('message')
agent_class = get_agent_class(agent_id=agent['id'], config=newsletter_config, email_config=email_config, agent_class = get_agent_class(agent_id=agent['id'],
config=newsletter_config, email_config=email_config,
subject=subject, body=body, message=message) subject=subject, body=body, message=message)
keys = {'id': newsletter_id} keys = {'id': newsletter_id}
values = {'agent_id': agent['id'], values = {'agent_id': agent['id'],
'agent_name': agent['name'], 'agent_name': agent['name'],
'agent_label': agent['label'], 'agent_label': agent['label'],
'id_name': kwargs.get('id_name', ''),
'friendly_name': kwargs.get('friendly_name', ''), 'friendly_name': kwargs.get('friendly_name', ''),
'newsletter_config': json.dumps(agent_class.config), 'newsletter_config': json.dumps(agent_class.config),
'email_config': json.dumps(agent_class.email_config), 'email_config': json.dumps(agent_class.email_config),
@@ -267,8 +274,11 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
def serve_template(templatename, **kwargs): def serve_template(templatename, **kwargs):
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/') if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES) template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
else:
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES)
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h']) _hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
@@ -299,22 +309,27 @@ class Newsletter(object):
'time_frame': 7, 'time_frame': 7,
'time_frame_units': 'days', 'time_frame_units': 'days',
'formatted': 1, 'formatted': 1,
'notifier_id': 0} 'notifier_id': 0,
'filename': '',
'save_only': 0}
_DEFAULT_EMAIL_CONFIG = EMAIL().return_default_config() _DEFAULT_EMAIL_CONFIG = EMAIL().return_default_config()
_DEFAULT_EMAIL_CONFIG['from_name'] = 'Tautulli Newsletter' _DEFAULT_EMAIL_CONFIG['from_name'] = 'Tautulli Newsletter'
_DEFAULT_EMAIL_CONFIG['notifier_id'] = 0 _DEFAULT_EMAIL_CONFIG['notifier_id'] = 0
_DEFAULT_SUBJECT = 'Tautulli Newsletter' _DEFAULT_SUBJECT = 'Tautulli Newsletter'
_DEFAULT_BODY = 'View the newsletter here: {newsletter_url}' _DEFAULT_BODY = 'View the newsletter here: {newsletter_url}'
_DEFAULT_MESSAGE = '' _DEFAULT_MESSAGE = ''
_DEFAULT_FILENAME = 'newsletter_{newsletter_uuid}.html'
_TEMPLATE_MASTER = '' _TEMPLATE_MASTER = ''
_TEMPLATE = '' _TEMPLATE = ''
def __init__(self, config=None, email_config=None, start_date=None, end_date=None, def __init__(self, newsletter_id=None, newsletter_id_name=None, config=None, email_config=None,
subject=None, body=None, message=None): start_date=None, end_date=None, subject=None, body=None, message=None):
self.config = self.set_config(config=config, default=self._DEFAULT_CONFIG) self.config = self.set_config(config=config, default=self._DEFAULT_CONFIG)
self.email_config = self.set_config(config=email_config, default=self._DEFAULT_EMAIL_CONFIG) self.email_config = self.set_config(config=email_config, default=self._DEFAULT_EMAIL_CONFIG)
self.uuid = generate_newsletter_uuid() self.uuid = generate_newsletter_uuid()
self.newsletter_id = newsletter_id
self.newsletter_id_name = newsletter_id_name or ''
self.start_date = None self.start_date = None
self.end_date = None self.end_date = None
@@ -346,7 +361,13 @@ class Newsletter(object):
self.subject = subject or self._DEFAULT_SUBJECT self.subject = subject or self._DEFAULT_SUBJECT
self.body = body or self._DEFAULT_BODY self.body = body or self._DEFAULT_BODY
self.message = message or self._DEFAULT_MESSAGE self.message = message or self._DEFAULT_MESSAGE
self.filename = self.config['filename'] or self._DEFAULT_FILENAME
if not self.filename.endswith('.html'):
self.filename += '.html'
self.subject_formatted, self.body_formatted, self.message_formatted = self.build_text() self.subject_formatted, self.body_formatted, self.message_formatted = self.build_text()
self.filename_formatted = self.build_filename()
self.data = {} self.data = {}
self.newsletter = None self.newsletter = None
@@ -421,13 +442,15 @@ class Newsletter(object):
return False return False
self._save() self._save()
if self.config['save_only']:
return True
return self._send() return self._send()
def _save(self): def _save(self):
newsletter_file = 'newsletter_%s-%s_%s.html' % (self.start_date.format('YYYYMMDD'), newsletter_file = self.filename_formatted
self.end_date.format('YYYYMMDD'), newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR or os.path.join(plexpy.DATA_DIR, 'newsletters')
self.uuid)
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file) newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
# In case the user has deleted it manually # In case the user has deleted it manually
@@ -440,9 +463,9 @@ class Newsletter(object):
if '<!-- IGNORE SAVE -->' not in line: if '<!-- IGNORE SAVE -->' not in line:
n_file.write(line + '\r\n') n_file.write(line + '\r\n')
logger.info(u"Tautulli Newsletters :: %s newsletter saved to %s" % (self.NAME, newsletter_file)) logger.info(u"Tautulli Newsletters :: %s newsletter saved to '%s'" % (self.NAME, newsletter_file))
except OSError as e: except OSError as e:
logger.error(u"Tautulli Newsletters :: Failed to save %s newsletter to %s: %s" logger.error(u"Tautulli Newsletters :: Failed to save %s newsletter to '%s': %s"
% (self.NAME, newsletter_file, e)) % (self.NAME, newsletter_file, e))
def _send(self): def _send(self):
@@ -475,7 +498,10 @@ class Newsletter(object):
def _build_params(self): def _build_params(self):
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT) date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
base_url = plexpy.CONFIG.HTTP_BASE_URL or helpers.get_plexpy_url() if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
else:
base_url = helpers.get_plexpy_url() + '/newsletter/'
parameters = { parameters = {
'server_name': plexpy.CONFIG.PMS_NAME, 'server_name': plexpy.CONFIG.PMS_NAME,
@@ -484,8 +510,11 @@ class Newsletter(object):
'week_number': self.start_date.isocalendar()[1], 'week_number': self.start_date.isocalendar()[1],
'newsletter_time_frame': self.config['time_frame'], 'newsletter_time_frame': self.config['time_frame'],
'newsletter_time_frame_units': self.config['time_frame_units'], 'newsletter_time_frame_units': self.config['time_frame_units'],
'newsletter_url': base_url.rstrip('/') + plexpy.HTTP_ROOT + 'newsletter/' + self.uuid, 'newsletter_url': base_url + self.uuid,
'newsletter_uuid': self.uuid 'newsletter_static_url': base_url + 'id/' + self.newsletter_id_name,
'newsletter_uuid': self.uuid,
'newsletter_id': self.newsletter_id,
'newsletter_id_name': self.newsletter_id_name
} }
return parameters return parameters
@@ -529,6 +558,23 @@ class Newsletter(object):
return subject, body, message return subject, body, message
def build_filename(self):
from notification_handler import CustomFormatter
custom_formatter = CustomFormatter()
try:
filename = custom_formatter.format(unicode(self.filename), **self.parameters)
except LookupError as e:
logger.error(
u"Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e)
filename = unicode(self._DEFAULT_FILENAME).format(**self.parameters)
except Exception as e:
logger.error(
u"Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
filename = unicode(self._DEFAULT_FILENAME).format(**self.parameters)
return filename
def return_config_options(self): def return_config_options(self):
return self._return_config_options() return self._return_config_options()
@@ -692,7 +738,7 @@ class RecentlyAdded(Newsletter):
artists = recently_added.get('artist', []) artists = recently_added.get('artist', [])
albums = [a for artist in artists for a in artist['album']] albums = [a for artist in artists for a in artist['album']]
if self.is_preview or plexpy.CONFIG.NEWSLETTER_SELF_HOSTED: if self.is_preview or helpers.get_img_service(include_self=True) == 'self-hosted':
for item in movies + shows + albums: for item in movies + shows + albums:
if item['media_type'] == 'album': if item['media_type'] == 'album':
height = 150 height = 150
@@ -714,7 +760,7 @@ class RecentlyAdded(Newsletter):
item['poster_url'] = '' item['poster_url'] = ''
item['art_url'] = '' item['art_url'] = ''
else: elif helpers.get_img_service():
# Upload posters and art to image hosting service # Upload posters and art to image hosting service
for item in movies + shows + albums: for item in movies + shows + albums:
if item['media_type'] == 'album': if item['media_type'] == 'album':
@@ -797,4 +843,4 @@ class RecentlyAdded(Newsletter):
} }
] ]
return config_options + additional_config return additional_config + config_options

View File

@@ -317,7 +317,9 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
else: else:
evaluated_logic = all(evaluated_conditions[1:]) evaluated_logic = all(evaluated_conditions[1:])
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic)) logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '{}'. Conditions: {}.".format(
evaluated_logic, evaluated_conditions[1:]))
return evaluated_logic return evaluated_logic
return True return True
@@ -1096,7 +1098,10 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
service = helpers.get_img_service() service = helpers.get_img_service()
if service == 'cloudinary': if service is None:
return img_info
elif service == 'cloudinary':
if fallback == 'cover': if fallback == 'cover':
w, h = 1000, 1000 w, h = 1000, 1000
elif fallback == 'art': elif fallback == 'art':
@@ -1132,7 +1137,7 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
elif not database_img_info and img: elif not database_img_info and img:
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_image(**image_info) result = pms_connect.get_image(refresh=True, **image_info)
if result and result[0]: if result and result[0]:
img_url = delete_hash = '' img_url = delete_hash = ''

View File

@@ -1310,26 +1310,30 @@ class EMAIL(Notifier):
recipients = self.config['to'] + self.config['cc'] + self.config['bcc'] recipients = self.config['to'] + self.config['cc'] + self.config['bcc']
success = False
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
try: try:
mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port']) mailserver.ehlo()
if self.config['tls']: if self.config['tls']:
mailserver.starttls() mailserver.starttls()
mailserver.ehlo()
mailserver.ehlo()
if self.config['smtp_user']: if self.config['smtp_user']:
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password'])) mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
mailserver.sendmail(self.config['from'], recipients, msg.as_string()) mailserver.sendmail(self.config['from'], recipients, msg.as_string())
mailserver.quit() success = True
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
return True
except Exception as e: except Exception as e:
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e)) logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
return False
finally:
mailserver.quit()
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
return success
def get_user_emails(self): def get_user_emails(self):
emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']} emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']}

View File

@@ -2436,7 +2436,7 @@ class PmsConnect(object):
return labels_list return labels_list
def get_image(self, img=None, width=1000, height=1500, opacity=None, background=None, blur=None, def get_image(self, img=None, width=1000, height=1500, opacity=None, background=None, blur=None,
img_format='png', clip=False, **kwargs): img_format='png', clip=False, refresh=False, **kwargs):
""" """
Return image data as array. Return image data as array.
Array contains the image content type and image binary Array contains the image content type and image binary
@@ -2454,6 +2454,9 @@ class PmsConnect(object):
height = height or 1500 height = height or 1500
if img: if img:
if refresh:
img = '{}/{}'.format(img.rstrip('/'), int(time.time()))
if clip: if clip:
params = {'url': '%s&%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))} params = {'url': '%s&%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
else: else:
@@ -2544,7 +2547,7 @@ class PmsConnect(object):
metadata = self.get_metadata_details(rating_key=rating_key) metadata = self.get_metadata_details(rating_key=rating_key)
search_results_list[metadata['media_type']].append(metadata) search_results_list[metadata['media_type']].append(metadata)
output = {'results_count': sum(len(s) for s in search_results_list.items()), output = {'results_count': sum(len(s) for s in search_results_list.values()),
'results_list': search_results_list 'results_list': search_results_list
} }

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.1.2-beta" PLEXPY_RELEASE_VERSION = "v2.1.6-beta"

View File

@@ -2755,7 +2755,8 @@ class WebInterface(object):
"tvmaze_lookup": checked(plexpy.CONFIG.TVMAZE_LOOKUP), "tvmaze_lookup": checked(plexpy.CONFIG.TVMAZE_LOOKUP),
"show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS, "show_advanced_settings": plexpy.CONFIG.SHOW_ADVANCED_SETTINGS,
"newsletter_dir": plexpy.CONFIG.NEWSLETTER_DIR, "newsletter_dir": plexpy.CONFIG.NEWSLETTER_DIR,
"newsletter_self_hosted": checked(plexpy.CONFIG.NEWSLETTER_SELF_HOSTED) "newsletter_self_hosted": checked(plexpy.CONFIG.NEWSLETTER_SELF_HOSTED),
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
} }
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs) return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
@@ -3962,13 +3963,20 @@ class WebInterface(object):
return return
if rating_key and not img: if rating_key and not img:
img = '/library/metadata/%s/thumb/1337' % rating_key if fallback == 'art':
img = '/library/metadata/{}/art'.format(rating_key)
else:
img = '/library/metadata/{}/thumb'.format(rating_key)
img_string = img.rsplit('/', 1)[0] if '/library/metadata' in img else img img_split = img.split('/')
img_string = '{}{}{}{}{}{}'.format(img_string, width, height, opacity, background, blur) img = '/'.join(img_split[:5])
rating_key = rating_key or img_split[3]
fp = hashlib.md5(img_string).hexdigest() img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
fp += '.%s' % img_format # we want to be able to preview the thumbs plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
img_hash = hashlib.sha256(img_string).hexdigest()
fp = '{}.{}'.format(img_hash, img_format) # we want to be able to preview the thumbs
c_dir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images') c_dir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images')
ffp = os.path.join(c_dir, fp) ffp = os.path.join(c_dir, fp)
@@ -3994,7 +4002,8 @@ class WebInterface(object):
background=background, background=background,
blur=blur, blur=blur,
img_format=img_format, img_format=img_format,
clip=clip) clip=clip,
refresh=refresh)
if result and result[0]: if result and result[0]:
cherrypy.response.headers['Content-type'] = result[1] cherrypy.response.headers['Content-type'] = result[1]
@@ -5653,10 +5662,18 @@ class WebInterface(object):
except NotFound: except NotFound:
return return
cherrypy.response.headers['Cache-Control'] = 'max-age=2592000' # 30 days
return self.image(args[1], refresh=True) return self.image(args[1], refresh=True)
newsletter_uuid = args[0] if len(args) >= 2 and args[0] == 'id':
newsletter = newsletter_handler.get_newsletter(newsletter_uuid=newsletter_uuid) newsletter_id_name = args[1]
newsletter_uuid = None
else:
newsletter_id_name = None
newsletter_uuid = args[0]
newsletter = newsletter_handler.get_newsletter(newsletter_uuid=newsletter_uuid,
newsletter_id_name=newsletter_id_name)
return newsletter return newsletter
@cherrypy.expose @cherrypy.expose
@@ -5675,7 +5692,9 @@ class WebInterface(object):
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id) newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
if newsletter: if newsletter:
newsletter_agent = newsletters.get_agent_class(agent_id=newsletter['agent_id'], newsletter_agent = newsletters.get_agent_class(newsletter_id=newsletter_id,
newsletter_id_name=newsletter['id_name'],
agent_id=newsletter['agent_id'],
config=newsletter['config'], config=newsletter['config'],
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,