# 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 . 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