Initial newsletter support

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

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