mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-20 05:13:21 -07:00
Initial newsletter support
This commit is contained in:
parent
b73d2ff1f7
commit
0f39201774
15 changed files with 2454 additions and 123 deletions
537
plexpy/newsletters.py
Normal file
537
plexpy/newsletters.py
Normal file
|
@ -0,0 +1,537 @@
|
|||
# This file is part of Tautulli.
|
||||
#
|
||||
# Tautulli is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Tautulli is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import arrow
|
||||
import json
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
from itertools import groupby
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import plexpy
|
||||
import database
|
||||
import helpers
|
||||
import logger
|
||||
import notification_handler
|
||||
import pmsconnect
|
||||
import request
|
||||
|
||||
|
||||
AGENT_IDS = {
|
||||
'recently_added': 0
|
||||
}
|
||||
|
||||
|
||||
def available_newsletter_agents():
|
||||
agents = [
|
||||
{
|
||||
'label': 'Recently Added',
|
||||
'name': 'recently_added',
|
||||
'id': AGENT_IDS['recently_added']
|
||||
}
|
||||
]
|
||||
|
||||
return agents
|
||||
|
||||
|
||||
def get_agent_class(agent_id=None, config=None):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
|
||||
if agent_id == 0:
|
||||
return RecentlyAdded(config=config)
|
||||
else:
|
||||
return Newsletter(config=config)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_newsletter_agents():
|
||||
return tuple(a['name'] for a in sorted(available_newsletter_agents(), key=lambda k: k['label']))
|
||||
|
||||
|
||||
def get_newsletters(newsletter_id=None):
|
||||
where = where_id = ''
|
||||
args = []
|
||||
|
||||
if newsletter_id:
|
||||
where = 'WHERE '
|
||||
if newsletter_id:
|
||||
where_id += 'id = ?'
|
||||
args.append(newsletter_id)
|
||||
where += ' AND '.join([w for w in [where_id] if w])
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select('SELECT id, agent_id, agent_name, agent_label, '
|
||||
'friendly_name, active FROM newsletters %s' % where, args=args)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def delete_newsletter(newsletter_id=None):
|
||||
db = database.MonitorDatabase()
|
||||
|
||||
if str(newsletter_id).isdigit():
|
||||
logger.debug(u"Tautulli Newsletters :: Deleting newsletter_id %s from the database."
|
||||
% newsletter_id)
|
||||
result = db.action('DELETE FROM newsletters WHERE id = ?', args=[newsletter_id])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_newsletter_config(newsletter_id=None):
|
||||
if str(newsletter_id).isdigit():
|
||||
newsletter_id = int(newsletter_id)
|
||||
else:
|
||||
logger.error(u"Tautulli Newsletters :: Unable to retrieve newsletter config: invalid newsletter_id %s."
|
||||
% newsletter_id)
|
||||
return None
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select_single('SELECT * FROM newsletters WHERE id = ?', args=[newsletter_id])
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = json.loads(result.pop('newsletter_config') or '{}')
|
||||
newsletter_agent = get_agent_class(agent_id=result['agent_id'], config=config)
|
||||
newsletter_config = newsletter_agent.return_config_options()
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e)
|
||||
return
|
||||
|
||||
result['config'] = config
|
||||
result['config_options'] = newsletter_config
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def add_newsletter_config(agent_id=None, **kwargs):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
else:
|
||||
logger.error(u"Tautulli Newsletters :: Unable to add new newsletter: invalid agent_id %s."
|
||||
% agent_id)
|
||||
return False
|
||||
|
||||
agent = next((a for a in available_newsletter_agents() if a['id'] == agent_id), None)
|
||||
|
||||
if not agent:
|
||||
logger.error(u"Tautulli Newsletters :: Unable to retrieve new newsletter agent: invalid agent_id %s."
|
||||
% agent_id)
|
||||
return False
|
||||
|
||||
keys = {'id': None}
|
||||
values = {'agent_id': agent['id'],
|
||||
'agent_name': agent['name'],
|
||||
'agent_label': agent['label'],
|
||||
'friendly_name': '',
|
||||
'newsletter_config': json.dumps(get_agent_class(agent_id=agent['id']).config)
|
||||
}
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
try:
|
||||
db.upsert(table_name='newsletters', key_dict=keys, value_dict=values)
|
||||
newsletter_id = db.last_insert_id()
|
||||
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
|
||||
% (agent['label'], newsletter_id))
|
||||
return newsletter_id
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
|
||||
return False
|
||||
|
||||
|
||||
def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
else:
|
||||
logger.error(u"Tautulli Newsletters :: Unable to set exisiting newsletter: invalid agent_id %s."
|
||||
% agent_id)
|
||||
return False
|
||||
|
||||
agent = next((a for a in available_newsletter_agents() if a['id'] == agent_id), None)
|
||||
|
||||
if not agent:
|
||||
logger.error(u"Tautulli Newsletters :: Unable to retrieve existing newsletter agent: invalid agent_id %s."
|
||||
% agent_id)
|
||||
return False
|
||||
|
||||
config_prefix = agent['name'] + '_'
|
||||
|
||||
newsletter_config = {k[len(config_prefix):]: kwargs.pop(k)
|
||||
for k in kwargs.keys() if k.startswith(config_prefix)}
|
||||
newsletter_config = get_agent_class(agent['id']).set_config(config=newsletter_config)
|
||||
|
||||
keys = {'id': newsletter_id}
|
||||
values = {'agent_id': agent['id'],
|
||||
'agent_name': agent['name'],
|
||||
'agent_label': agent['label'],
|
||||
'friendly_name': kwargs.get('friendly_name', ''),
|
||||
'newsletter_config': json.dumps(newsletter_config),
|
||||
'cron': kwargs.get('cron'),
|
||||
'active': kwargs.get('active')
|
||||
}
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
try:
|
||||
db.upsert(table_name='newsletters', key_dict=keys, value_dict=values)
|
||||
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
|
||||
% (agent['label'], newsletter_id))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
|
||||
return False
|
||||
|
||||
|
||||
def send_newsletter(newsletter_id=None, newsletter_log_id=None, **kwargs):
|
||||
newsletter_config = get_newsletter_config(newsletter_id=newsletter_id)
|
||||
if newsletter_config:
|
||||
agent = get_agent_class(agent_id=newsletter_config['agent_id'],
|
||||
config=newsletter_config['config'])
|
||||
return agent.send(newsletter_log_id=newsletter_log_id, **kwargs)
|
||||
else:
|
||||
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
|
||||
|
||||
|
||||
def serve_template(templatename, **kwargs):
|
||||
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
||||
template_dir = os.path.join(str(interface_dir), 'newsletters')
|
||||
|
||||
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
|
||||
|
||||
try:
|
||||
template = _hplookup.get_template(templatename)
|
||||
return template.render(**kwargs)
|
||||
except:
|
||||
return exceptions.html_error_template().render()
|
||||
|
||||
|
||||
class Newsletter(object):
|
||||
NAME = ''
|
||||
_DEFAULT_CONFIG = {}
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = {}
|
||||
self.set_config(config)
|
||||
|
||||
def set_config(self, config=None):
|
||||
self.config = self._validate_config(config)
|
||||
return self.config
|
||||
|
||||
def _validate_config(self, config=None):
|
||||
if config is None:
|
||||
return self._DEFAULT_CONFIG
|
||||
|
||||
new_config = {}
|
||||
for k, v in self._DEFAULT_CONFIG.iteritems():
|
||||
if isinstance(v, int):
|
||||
new_config[k] = helpers.cast_to_int(config.get(k, v))
|
||||
else:
|
||||
new_config[k] = config.get(k, v)
|
||||
|
||||
return new_config
|
||||
|
||||
def preview(self, **kwargs):
|
||||
pass
|
||||
|
||||
def send(self, **kwargs):
|
||||
pass
|
||||
|
||||
def make_request(self, url, method='POST', **kwargs):
|
||||
response, err_msg, req_msg = request.request_response2(url, method, **kwargs)
|
||||
|
||||
if response and not err_msg:
|
||||
logger.info(u"Tautulli Newsletters :: {name} notification sent.".format(name=self.NAME))
|
||||
return True
|
||||
|
||||
else:
|
||||
verify_msg = ""
|
||||
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
||||
verify_msg = " Verify you notification newsletter agent settings are correct."
|
||||
|
||||
logger.error(u"Tautulli Newsletters :: {name} notification failed.{}".format(verify_msg, name=self.NAME))
|
||||
|
||||
if err_msg:
|
||||
logger.error(u"Tautulli Newsletters :: {}".format(err_msg))
|
||||
|
||||
if req_msg:
|
||||
logger.debug(u"Tautulli Newsletters :: Request response: {}".format(req_msg))
|
||||
|
||||
return False
|
||||
|
||||
def return_config_options(self):
|
||||
config_options = []
|
||||
return config_options
|
||||
|
||||
|
||||
class RecentlyAdded(Newsletter):
|
||||
"""
|
||||
Recently Added Newsletter
|
||||
"""
|
||||
NAME = 'Recently Added'
|
||||
_DEFAULT_CONFIG = {'last_days': 7,
|
||||
'incl_movies': 1,
|
||||
'incl_shows': 1,
|
||||
'incl_artists': 1
|
||||
}
|
||||
_TEMPLATE = 'recently_added.html'
|
||||
|
||||
def __init__(self, config=None):
|
||||
super(RecentlyAdded, self).__init__(config)
|
||||
|
||||
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
|
||||
|
||||
self.end_time = int(time.time())
|
||||
self.start_time = self.end_time - self.config['last_days']*24*60*60
|
||||
self.end_date = arrow.get(self.end_time).format(date_format)
|
||||
self.start_date = arrow.get(self.start_time).format(date_format)
|
||||
|
||||
self.plexpy_config = {
|
||||
'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'pms_web_url': plexpy.CONFIG.PMS_WEB_URL
|
||||
}
|
||||
|
||||
self.recently_added = {}
|
||||
|
||||
def _get_recently_added(self, media_type=None):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
|
||||
recently_added = []
|
||||
done = False
|
||||
start = 0
|
||||
|
||||
while not done:
|
||||
recent_items = pms_connect.get_recently_added_details(start=str(start), count='10', type=media_type)
|
||||
filtered_items = [i for i in recent_items['recently_added']
|
||||
if helpers.cast_to_int(i['added_at']) > self.start_time]
|
||||
if len(filtered_items) < 10:
|
||||
done = True
|
||||
else:
|
||||
start += 10
|
||||
|
||||
recently_added.extend(filtered_items)
|
||||
|
||||
if media_type == 'show':
|
||||
shows_list = []
|
||||
show_rating_keys = []
|
||||
for item in recently_added:
|
||||
if item['media_type'] == 'show':
|
||||
show_rating_key = item['rating_key']
|
||||
elif item['media_type'] == 'season':
|
||||
show_rating_key = item['parent_rating_key']
|
||||
elif item['media_type'] == 'episode':
|
||||
show_rating_key = item['grandparent_rating_key']
|
||||
|
||||
if show_rating_key in show_rating_keys:
|
||||
continue
|
||||
|
||||
show_metadata = pms_connect.get_metadata_details(show_rating_key, media_info=False)
|
||||
children = pms_connect.get_item_children(show_rating_key, get_grandchildren=True)
|
||||
filtered_children = [i for i in children['children_list']
|
||||
if helpers.cast_to_int(i['added_at']) > self.start_time]
|
||||
filtered_children.sort(key=lambda x: x['parent_media_index'])
|
||||
|
||||
seasons = []
|
||||
for k, v in groupby(filtered_children, key=lambda x: x['parent_media_index']):
|
||||
episodes = list(v)
|
||||
num, num00 = notification_handler.format_group_index(
|
||||
[helpers.cast_to_int(d['media_index']) for d in episodes])
|
||||
|
||||
seasons.append({'media_index': k,
|
||||
'episode_range': num00,
|
||||
'episode_count': len(episodes),
|
||||
'episode': episodes})
|
||||
|
||||
num, num00 = notification_handler.format_group_index(
|
||||
[helpers.cast_to_int(d['media_index']) for d in seasons])
|
||||
|
||||
show_metadata['season_range'] = num00
|
||||
show_metadata['season_count'] = len(seasons)
|
||||
show_metadata['season'] = seasons
|
||||
|
||||
shows_list.append(show_metadata)
|
||||
show_rating_keys.append(show_rating_key)
|
||||
|
||||
recently_added = shows_list
|
||||
|
||||
if media_type == 'artist':
|
||||
artists_list = []
|
||||
artist_rating_keys = []
|
||||
for item in recently_added:
|
||||
if item['media_type'] == 'artist':
|
||||
artist_rating_key = item['rating_key']
|
||||
elif item['media_type'] == 'album':
|
||||
artist_rating_key = item['parent_rating_key']
|
||||
elif item['media_type'] == 'track':
|
||||
artist_rating_key = item['grandparent_rating_key']
|
||||
|
||||
if artist_rating_key in artist_rating_keys:
|
||||
continue
|
||||
|
||||
artist_metadata = pms_connect.get_metadata_details(artist_rating_key, media_info=False)
|
||||
children = pms_connect.get_item_children(artist_rating_key)
|
||||
filtered_children = [i for i in children['children_list']
|
||||
if helpers.cast_to_int(i['added_at']) > self.start_time]
|
||||
filtered_children.sort(key=lambda x: x['added_at'])
|
||||
|
||||
albums = []
|
||||
for a in filtered_children:
|
||||
album_metadata = pms_connect.get_metadata_details(a['rating_key'], media_info=False)
|
||||
album_metadata['track_count'] = helpers.cast_to_int(album_metadata['children_count'])
|
||||
albums.append(album_metadata)
|
||||
|
||||
artist_metadata['album_count'] = len(albums)
|
||||
artist_metadata['album'] = albums
|
||||
|
||||
artists_list.append(artist_metadata)
|
||||
artist_rating_keys.append(artist_rating_key)
|
||||
|
||||
recently_added = artists_list
|
||||
|
||||
return recently_added
|
||||
|
||||
def get_recently_added(self):
|
||||
if self.config['incl_movies']:
|
||||
self.recently_added['movie'] = self._get_recently_added('movie')
|
||||
if self.config['incl_shows']:
|
||||
self.recently_added['show'] = self._get_recently_added('show')
|
||||
if self.config['incl_artists']:
|
||||
self.recently_added['artist'] = self._get_recently_added('artist')
|
||||
|
||||
return self.recently_added
|
||||
|
||||
def preview(self, **kwargs):
|
||||
self.get_recently_added()
|
||||
|
||||
return serve_template(
|
||||
templatename=self._TEMPLATE,
|
||||
title=self.NAME,
|
||||
recently_added=self.recently_added,
|
||||
start_date=self.start_date,
|
||||
end_date=self.end_date,
|
||||
plexpy_config=self.plexpy_config
|
||||
)
|
||||
|
||||
def send(self, **kwargs):
|
||||
if not subject or not body:
|
||||
return
|
||||
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
|
||||
data = {'content': text}
|
||||
if self.config['username']:
|
||||
data['username'] = self.config['username']
|
||||
if self.config['avatar_url']:
|
||||
data['avatar_url'] = self.config['avatar_url']
|
||||
if self.config['tts']:
|
||||
data['tts'] = True
|
||||
|
||||
if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
|
||||
# Grab formatted metadata
|
||||
pretty_metadata = PrettyMetadata(kwargs['parameters'])
|
||||
|
||||
if pretty_metadata.media_type == 'movie':
|
||||
provider = self.config['movie_provider']
|
||||
elif pretty_metadata.media_type in ('show', 'season', 'episode'):
|
||||
provider = self.config['tv_provider']
|
||||
elif pretty_metadata.media_type in ('artist', 'album', 'track'):
|
||||
provider = self.config['music_provider']
|
||||
else:
|
||||
provider = None
|
||||
|
||||
poster_url = pretty_metadata.get_poster_url()
|
||||
provider_name = pretty_metadata.get_provider_name(provider)
|
||||
provider_link = pretty_metadata.get_provider_link(provider)
|
||||
title = pretty_metadata.get_title('\xc2\xb7'.decode('utf8'))
|
||||
description = pretty_metadata.get_description()
|
||||
plex_url = pretty_metadata.get_plex_url()
|
||||
|
||||
# Build Discord post attachment
|
||||
attachment = {'title': title
|
||||
}
|
||||
|
||||
if self.config['color']:
|
||||
hex_match = re.match(r'^#([0-9a-fA-F]{3}){1,2}$', self.config['color'])
|
||||
if hex_match:
|
||||
hex = hex_match.group(0).lstrip('#')
|
||||
hex = ''.join(h * 2 for h in hex) if len(hex) == 3 else hex
|
||||
attachment['color'] = helpers.hex_to_int(hex)
|
||||
|
||||
if self.config['incl_thumbnail']:
|
||||
attachment['thumbnail'] = {'url': poster_url}
|
||||
else:
|
||||
attachment['image'] = {'url': poster_url}
|
||||
|
||||
if self.config['incl_description'] or pretty_metadata.media_type in ('artist', 'album', 'track'):
|
||||
attachment['description'] = description
|
||||
|
||||
fields = []
|
||||
if provider_link:
|
||||
attachment['url'] = provider_link
|
||||
fields.append({'name': 'View Details',
|
||||
'value': '[%s](%s)' % (provider_name, provider_link.encode('utf-8')),
|
||||
'inline': True})
|
||||
if self.config['incl_pmslink']:
|
||||
fields.append({'name': 'View Details',
|
||||
'value': '[Plex Web](%s)' % plex_url.encode('utf-8'),
|
||||
'inline': True})
|
||||
if fields:
|
||||
attachment['fields'] = fields
|
||||
|
||||
data['embeds'] = [attachment]
|
||||
|
||||
headers = {'Content-type': 'application/json'}
|
||||
params = {'wait': True}
|
||||
|
||||
return self.make_request(self.config['hook'], params=params, headers=headers, json=data)
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Number of Days',
|
||||
'value': self.config['last_days'],
|
||||
'name': 'recently_added_last_days',
|
||||
'description': 'The past number of days to include in the newsletter.',
|
||||
'input_type': 'number'
|
||||
},
|
||||
{'label': 'Include Movies',
|
||||
'value': self.config['incl_movies'],
|
||||
'description': 'Include recently added movies in the newsletter.',
|
||||
'name': 'recently_added_incl_movies',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Include TV Shows',
|
||||
'value': self.config['incl_shows'],
|
||||
'description': 'Include recently added TV shows in the newsletter.',
|
||||
'name': 'recently_added_incl_shows',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Include Music',
|
||||
'value': self.config['incl_artists'],
|
||||
'description': 'Include recently added music in the newsletter.',
|
||||
'name': 'recently_added_incl_artists',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
return config_option
|
Loading…
Add table
Add a link
Reference in a new issue