Initial newsletter support

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

View file

@ -620,6 +620,14 @@ def dbcheck():
'custom_conditions TEXT, custom_conditions_logic TEXT)'
)
# newsletters table :: This table keeps record of the newsletter settings
c_db.execute(
'CREATE TABLE IF NOT EXISTS newsletters (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'agent_id INTEGER, agent_name TEXT, agent_label TEXT, '
'friendly_name TEXT, newsletter_config TEXT, '
'cron TEXT NOT NULL DEFAULT "0 0 * * 0", active INTEGER DEFAULT 0)'
)
# poster_urls table :: This table keeps record of the notification poster urls
c_db.execute(
'CREATE TABLE IF NOT EXISTS poster_urls (id INTEGER PRIMARY KEY AUTOINCREMENT, '

View file

@ -965,4 +965,12 @@ def get_plexpy_url(hostname=None):
else:
root = ''
return scheme + '://' + hostname + port + root
return scheme + '://' + hostname + port + root
def momentjs_to_arrow(format, duration=False):
invalid_formats = ['Mo', 'DDDo', 'do']
if duration:
invalid_formats += ['A', 'a']
for f in invalid_formats:
format = format.replace(f, '')
return format

537
plexpy/newsletters.py Normal file
View file

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

View file

@ -448,9 +448,9 @@ def set_notify_success(notification_id):
def build_media_notify_params(notify_action=None, session=None, timeline=None, manual_trigger=False, **kwargs):
# Get time formats
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
time_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT)
duration_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT, duration=True)
# Get metadata for the item
if session:

View file

@ -338,7 +338,7 @@ def get_agent_class(agent_id=None, config=None):
agent_id = int(agent_id)
if agent_id == 0:
return GROWL(config=config,)
return GROWL(config=config)
elif agent_id == 1:
return PROWL(config=config)
elif agent_id == 2:
@ -419,8 +419,8 @@ def get_notifiers(notifier_id=None, notify_action=None):
db = database.MonitorDatabase()
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
% (', '.join(notify_actions), where), args=args)
% (', '.join(notify_actions), where), args=args)
for item in result:
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
@ -431,9 +431,9 @@ def delete_notifier(notifier_id=None):
db = database.MonitorDatabase()
if str(notifier_id).isdigit():
logger.debug(u"Tautulli Notifiers :: Deleting notifier_id %s from the database." % notifier_id)
result = db.action('DELETE FROM notifiers WHERE id = ?',
args=[notifier_id])
logger.debug(u"Tautulli Notifiers :: Deleting notifier_id %s from the database."
% notifier_id)
result = db.action('DELETE FROM notifiers WHERE id = ?', args=[notifier_id])
return True
else:
return False
@ -443,12 +443,13 @@ def get_notifier_config(notifier_id=None):
if str(notifier_id).isdigit():
notifier_id = int(notifier_id)
else:
logger.error(u"Tautulli Notifiers :: Unable to retrieve notifier config: invalid notifier_id %s." % notifier_id)
logger.error(u"Tautulli Notifiers :: Unable to retrieve notifier config: invalid notifier_id %s."
% notifier_id)
return None
db = database.MonitorDatabase()
result = db.select_single('SELECT * FROM notifiers WHERE id = ?',
args=[notifier_id])
result = db.select_single('SELECT * FROM notifiers WHERE id = ?', args=[notifier_id])
if not result:
return None
@ -490,13 +491,15 @@ def add_notifier_config(agent_id=None, **kwargs):
if str(agent_id).isdigit():
agent_id = int(agent_id)
else:
logger.error(u"Tautulli Notifiers :: Unable to add new notifier: invalid agent_id %s." % agent_id)
logger.error(u"Tautulli Notifiers :: Unable to add new notifier: invalid agent_id %s."
% agent_id)
return False
agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
if not agent:
logger.error(u"Tautulli Notifiers :: Unable to retrieve new notification agent: invalid agent_id %s." % agent_id)
logger.error(u"Tautulli Notifiers :: Unable to retrieve new notification agent: invalid agent_id %s."
% agent_id)
return False
keys = {'id': None}
@ -521,7 +524,8 @@ def add_notifier_config(agent_id=None, **kwargs):
try:
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
notifier_id = db.last_insert_id()
logger.info(u"Tautulli Notifiers :: Added new notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
logger.info(u"Tautulli Notifiers :: Added new notification agent: %s (notifier_id %s)."
% (agent['label'], notifier_id))
blacklist_logger()
return notifier_id
except Exception as e:
@ -533,13 +537,15 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
if str(agent_id).isdigit():
agent_id = int(agent_id)
else:
logger.error(u"Tautulli Notifiers :: Unable to set exisiting notifier: invalid agent_id %s." % agent_id)
logger.error(u"Tautulli Notifiers :: Unable to set exisiting notifier: invalid agent_id %s."
% agent_id)
return False
agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
if not agent:
logger.error(u"Tautulli Notifiers :: Unable to retrieve existing notification agent: invalid agent_id %s." % agent_id)
logger.error(u"Tautulli Notifiers :: Unable to retrieve existing notification agent: invalid agent_id %s."
% agent_id)
return False
notify_actions = get_notify_actions()
@ -571,7 +577,8 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
db = database.MonitorDatabase()
try:
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)."
% (agent['label'], notifier_id))
blacklist_logger()
if agent['name'] == 'browser':
@ -743,6 +750,7 @@ class Notifier(object):
_DEFAULT_CONFIG = {}
def __init__(self, config=None):
self.config = {}
self.set_config(config)
def set_config(self, config=None):

View file

@ -139,6 +139,22 @@ class PmsConnect(object):
return request
def get_metadata_grandchildren(self, rating_key='', output_format=''):
"""
Return metadata for graandchildren of the request item.
Parameters required: rating_key { Plex ratingKey }
Optional parameters: output_format { dict, json }
Output: array
"""
uri = '/library/metadata/' + rating_key + '/grandchildren'
request = self.request_handler.make_request(uri=uri,
request_type='GET',
output_format=output_format)
return request
def get_recently_added(self, start='0', count='0', output_format=''):
"""
Return list of recently added items.
@ -171,22 +187,6 @@ class PmsConnect(object):
return request
def get_children_list(self, rating_key='', output_format=''):
"""
Return list of children in requested library item.
Parameters required: rating_key { ratingKey of parent }
Optional parameters: output_format { dict, json }
Output: array
"""
uri = '/library/metadata/' + rating_key + '/children'
request = self.request_handler.make_request(uri=uri,
request_type='GET',
output_format=output_format)
return request
def get_children_list_related(self, rating_key='', output_format=''):
"""
Return list of related children in requested collection item.
@ -470,59 +470,86 @@ class PmsConnect(object):
output = {'recently_added': []}
return output
recents_main = []
if a.getElementsByTagName('Directory'):
recents_main = a.getElementsByTagName('Directory')
for item in recents_main:
recent_items = {'media_type': helpers.get_xml_attr(item, 'type'),
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
'media_index': helpers.get_xml_attr(item, 'index'),
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
'section_id': section_id if section_id else helpers.get_xml_attr(item, 'librarySectionID'),
'library_name': helpers.get_xml_attr(item, 'librarySectionTitle'),
'year': helpers.get_xml_attr(item, 'year'),
'thumb': helpers.get_xml_attr(item, 'thumb'),
'parent_thumb': helpers.get_xml_attr(item, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(item, 'grandparentThumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt'),
'child_count': helpers.get_xml_attr(item, 'childCount')
}
recents_list.append(recent_items)
recents_main += a.getElementsByTagName('Directory')
if a.getElementsByTagName('Video'):
recents_main = a.getElementsByTagName('Video')
for item in recents_main:
recent_items = {'media_type': helpers.get_xml_attr(item, 'type'),
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(item, 'grandparentTitle'),
'sort_title': helpers.get_xml_attr(item, 'titleSort'),
'media_index': helpers.get_xml_attr(item, 'index'),
'parent_media_index': helpers.get_xml_attr(item, 'parentIndex'),
'section_id': section_id if section_id else helpers.get_xml_attr(item, 'librarySectionID'),
'library_name': helpers.get_xml_attr(item, 'librarySectionTitle'),
'year': helpers.get_xml_attr(item, 'year'),
'thumb': helpers.get_xml_attr(item, 'thumb'),
'parent_thumb': helpers.get_xml_attr(item, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(item, 'grandparentThumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt'),
'child_count': helpers.get_xml_attr(item, 'childCount')
}
recents_list.append(recent_items)
recents_main += a.getElementsByTagName('Video')
for m in recents_main:
directors = []
writers = []
actors = []
genres = []
labels = []
if m.getElementsByTagName('Director'):
for director in m.getElementsByTagName('Director'):
directors.append(helpers.get_xml_attr(director, 'tag'))
if m.getElementsByTagName('Writer'):
for writer in m.getElementsByTagName('Writer'):
writers.append(helpers.get_xml_attr(writer, 'tag'))
if m.getElementsByTagName('Role'):
for actor in m.getElementsByTagName('Role'):
actors.append(helpers.get_xml_attr(actor, 'tag'))
if m.getElementsByTagName('Genre'):
for genre in m.getElementsByTagName('Genre'):
genres.append(helpers.get_xml_attr(genre, 'tag'))
if m.getElementsByTagName('Label'):
for label in m.getElementsByTagName('Label'):
labels.append(helpers.get_xml_attr(label, 'tag'))
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
'rating_key': helpers.get_xml_attr(m, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(m, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(m, 'grandparentRatingKey'),
'title': helpers.get_xml_attr(m, 'title'),
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
'media_index': helpers.get_xml_attr(m, 'index'),
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
'studio': helpers.get_xml_attr(m, 'studio'),
'content_rating': helpers.get_xml_attr(m, 'contentRating'),
'summary': helpers.get_xml_attr(m, 'summary'),
'tagline': helpers.get_xml_attr(m, 'tagline'),
'rating': helpers.get_xml_attr(m, 'rating'),
'audience_rating': helpers.get_xml_attr(m, 'audienceRating'),
'user_rating': helpers.get_xml_attr(m, 'userRating'),
'duration': helpers.get_xml_attr(m, 'duration'),
'year': helpers.get_xml_attr(m, 'year'),
'thumb': helpers.get_xml_attr(m, 'thumb'),
'parent_thumb': helpers.get_xml_attr(m, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(m, 'grandparentThumb'),
'art': helpers.get_xml_attr(m, 'art'),
'banner': helpers.get_xml_attr(m, 'banner'),
'originally_available_at': helpers.get_xml_attr(m, 'originallyAvailableAt'),
'added_at': helpers.get_xml_attr(m, 'addedAt'),
'updated_at': helpers.get_xml_attr(m, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(m, 'lastViewedAt'),
'guid': helpers.get_xml_attr(m, 'guid'),
'directors': directors,
'writers': writers,
'actors': actors,
'genres': genres,
'labels': labels,
'full_title': helpers.get_xml_attr(m, 'title'),
'child_count': helpers.get_xml_attr(m, 'childCount')
}
recents_list.append(recent_item)
output = {'recently_added': sorted(recents_list, key=lambda k: k['added_at'], reverse=True)}
return output
def get_metadata_details(self, rating_key='', sync_id='', cache_key=None):
def get_metadata_details(self, rating_key='', sync_id='', cache_key=None, media_info=True):
"""
Return processed and validated metadata list for requested item.
@ -662,7 +689,8 @@ class PmsConnect(object):
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'show':
@ -708,7 +736,8 @@ class PmsConnect(object):
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'season':
@ -752,7 +781,8 @@ class PmsConnect(object):
'labels': show_details['labels'],
'collections': show_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'episode':
@ -796,7 +826,8 @@ class PmsConnect(object):
'labels': show_details['labels'],
'collections': show_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'artist':
@ -837,7 +868,8 @@ class PmsConnect(object):
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'album':
@ -881,7 +913,8 @@ class PmsConnect(object):
'labels': labels,
'collections': collections,
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'track':
@ -925,7 +958,8 @@ class PmsConnect(object):
'labels': album_details['labels'],
'collections': album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'photo_album':
@ -966,7 +1000,8 @@ class PmsConnect(object):
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'photo':
@ -1010,7 +1045,8 @@ class PmsConnect(object):
'labels': photo_album_details['labels'],
'collections': photo_album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title'))
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'collection':
@ -1055,7 +1091,8 @@ class PmsConnect(object):
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
}
elif metadata_type == 'clip':
@ -1102,7 +1139,7 @@ class PmsConnect(object):
else:
return {}
if metadata:
if metadata and media_info:
medias = []
media_items = metadata_main.getElementsByTagName('Media')
for media in media_items:
@ -1873,18 +1910,21 @@ class PmsConnect(object):
else:
return False
def get_item_children(self, rating_key=''):
def get_item_children(self, rating_key='', get_grandchildren=False):
"""
Return processed and validated children list.
Output: array
"""
children_data = self.get_children_list(rating_key, output_format='xml')
if get_grandchildren:
children_data = self.get_metadata_grandchildren(rating_key, output_format='xml')
else:
children_data = self.get_metadata_children(rating_key, output_format='xml')
try:
xml_head = children_data.getElementsByTagName('MediaContainer')
except Exception as e:
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_children_list: %s." % e)
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_item_children: %s." % e)
return []
children_list = []
@ -1907,21 +1947,72 @@ class PmsConnect(object):
if a.getElementsByTagName('Track'):
result_data = a.getElementsByTagName('Track')
section_id = helpers.get_xml_attr(a, 'librarySectionID')
if result_data:
for result in result_data:
children_output = {'section_id': section_id,
'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(result, 'parentRatingKey'),
'media_index': helpers.get_xml_attr(result, 'index'),
'title': helpers.get_xml_attr(result, 'title'),
'parent_title': helpers.get_xml_attr(result, 'parentTitle'),
'year': helpers.get_xml_attr(result, 'year'),
'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration')
}
for m in result_data:
directors = []
writers = []
actors = []
genres = []
labels = []
if m.getElementsByTagName('Director'):
for director in m.getElementsByTagName('Director'):
directors.append(helpers.get_xml_attr(director, 'tag'))
if m.getElementsByTagName('Writer'):
for writer in m.getElementsByTagName('Writer'):
writers.append(helpers.get_xml_attr(writer, 'tag'))
if m.getElementsByTagName('Role'):
for actor in m.getElementsByTagName('Role'):
actors.append(helpers.get_xml_attr(actor, 'tag'))
if m.getElementsByTagName('Genre'):
for genre in m.getElementsByTagName('Genre'):
genres.append(helpers.get_xml_attr(genre, 'tag'))
if m.getElementsByTagName('Label'):
for label in m.getElementsByTagName('Label'):
labels.append(helpers.get_xml_attr(label, 'tag'))
children_output = {'media_type': helpers.get_xml_attr(m, 'type'),
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
'rating_key': helpers.get_xml_attr(m, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(m, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(m, 'grandparentRatingKey'),
'title': helpers.get_xml_attr(m, 'title'),
'parent_title': helpers.get_xml_attr(m, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(m, 'grandparentTitle'),
'sort_title': helpers.get_xml_attr(m, 'titleSort'),
'media_index': helpers.get_xml_attr(m, 'index'),
'parent_media_index': helpers.get_xml_attr(m, 'parentIndex'),
'studio': helpers.get_xml_attr(m, 'studio'),
'content_rating': helpers.get_xml_attr(m, 'contentRating'),
'summary': helpers.get_xml_attr(m, 'summary'),
'tagline': helpers.get_xml_attr(m, 'tagline'),
'rating': helpers.get_xml_attr(m, 'rating'),
'audience_rating': helpers.get_xml_attr(m, 'audienceRating'),
'user_rating': helpers.get_xml_attr(m, 'userRating'),
'duration': helpers.get_xml_attr(m, 'duration'),
'year': helpers.get_xml_attr(m, 'year'),
'thumb': helpers.get_xml_attr(m, 'thumb'),
'parent_thumb': helpers.get_xml_attr(m, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(m, 'grandparentThumb'),
'art': helpers.get_xml_attr(m, 'art'),
'banner': helpers.get_xml_attr(m, 'banner'),
'originally_available_at': helpers.get_xml_attr(m, 'originallyAvailableAt'),
'added_at': helpers.get_xml_attr(m, 'addedAt'),
'updated_at': helpers.get_xml_attr(m, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(m, 'lastViewedAt'),
'guid': helpers.get_xml_attr(m, 'guid'),
'directors': directors,
'writers': writers,
'actors': actors,
'genres': genres,
'labels': labels,
'full_title': helpers.get_xml_attr(m, 'title')
}
children_list.append(children_output)
output = {'children_count': helpers.get_xml_attr(xml_head[0], 'size'),
@ -2157,7 +2248,7 @@ class PmsConnect(object):
if str(section_id).isdigit():
library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key, output_format='xml')
elif str(rating_key).isdigit():
library_data = self.get_children_list(str(rating_key), output_format='xml')
library_data = self.get_metadata_children(str(rating_key), output_format='xml')
else:
logger.warn(u"Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
return []

View file

@ -39,6 +39,7 @@ import http_handler
import libraries
import log_reader
import logger
import newsletters
import mobile_app
import notification_handler
import notifiers
@ -5285,3 +5286,238 @@ class WebInterface(object):
@requireAuth()
def get_plexpy_url(self, **kwargs):
return helpers.get_plexpy_url()
@cherrypy.expose
@requireAuth()
def newsletter(self, **kwargs):
news_letter = newsletters.Newsletter()
config = {
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL
}
return serve_template(templatename="newsletter_template.html",
title="Newsletter",
recently_added=news_letter.recently_added,
start_date=news_letter.start_date,
end_date=news_letter.end_date,
config=config)
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth()
def newsletter_raw(self, **kwargs):
news_letter = newsletters.Newsletter()
if news_letter.recently_added:
return news_letter.recently_added
else:
return None
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_newsletters(self, **kwargs):
""" Get a list of configured newsletters.
```
Required parameters:
None
Optional parameters:
None
Returns:
json:
[{"id": 1,
"agent_id": 13,
"agent_name": "recently_added",
"agent_label": "Recently Added",
"friendly_name": "",
"cron": "0 0 * * 1",
"active": 1
}
]
```
"""
result = newsletters.get_newsletters()
return result
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_newsletters_table(self, **kwargs):
result = newsletters.get_newsletters()
return serve_template(templatename="newsletters_table.html", newsletters_list=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def delete_newsletter(self, newsletter_id=None, **kwargs):
""" Remove a newsletter from the database.
```
Required parameters:
newsletter_id (int): The newsletter to delete
Optional parameters:
None
Returns:
None
```
"""
result = newsletters.delete_newsletter(newsletter_id=newsletter_id)
if result:
return {'result': 'success', 'message': 'Newsletter deleted successfully.'}
else:
return {'result': 'error', 'message': 'Failed to delete newsletter.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_newsletter_config(self, newsletter_id=None, **kwargs):
""" Get the configuration for an existing notification agent.
```
Required parameters:
newsletter_id (int): The newsletter config to retrieve
Optional parameters:
None
Returns:
json:
{"id": 1,
"agent_id": 13,
"agent_name": "recently_added",
"agent_label": "Recently Added",
"friendly_name": "",
"cron": "0 0 * * 1",
"active": 1
"config": {"last_days": 7,
"incl_movies": 1,
"incl_shows": 1,
"incl_artists": 1,
},
"config_options": [{...}, ...]
}
```
"""
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
return result
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_newsletter_config_modal(self, newsletter_id=None, **kwargs):
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
return serve_template(templatename="newsletter_config.html", newsletter=result)
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def add_newsletter_config(self, agent_id=None, **kwargs):
""" Add a new notification agent.
```
Required parameters:
agent_id (int): The newsletter type to add
Optional parameters:
None
Returns:
None
```
"""
result = newsletters.add_newsletter_config(agent_id=agent_id, **kwargs)
if result:
return {'result': 'success', 'message': 'Added newsletter.', 'newsletter_id': result}
else:
return {'result': 'error', 'message': 'Failed to add newsletter.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def set_newsletter_config(self, newsletter_id=None, agent_id=None, **kwargs):
""" Configure an exisitng notificaiton agent.
```
Required parameters:
newsletter_id (int): The newsletter config to update
agent_id (int): The newsletter type of the newsletter
Optional parameters:
Pass all the config options for the agent with the agent prefix:
e.g. For Recently Added: recently_added_last_days
recently_added_incl_movies
recently_added_incl_shows
recently_added_incl_artists
Returns:
None
```
"""
result = newsletters.set_newsletter_config(newsletter_id=newsletter_id,
agent_id=agent_id,
**kwargs)
if result:
return {'result': 'success', 'message': 'Saved newsletter.'}
else:
return {'result': 'error', 'message': 'Failed to save newsletter.'}
@cherrypy.expose
@requireAuth(member_of("admin"))
@addtoapi("notify")
def send_newsletter(self, newsletter_id=None, test=False, **kwargs):
""" Send a newsletter using Tautulli.
```
Required parameters:
newsletter_id (int): The ID number of the newsletter
Optional parameters:
None
Returns:
None
```
"""
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
test = 'test ' if test else ''
if newsletter_id:
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
if newsletter:
logger.debug(u"Sending %s%s newsletter." % (test, newsletter['agent_name']))
if newsletter_handler.send(newsletter_id=newsletter_id,
**kwargs):
return "Newsletter sent."
else:
return "Newsletter failed."
else:
logger.debug(u"Unable to send %snewsletter, invalid newsletter_id %s." % (test, newsletter_id))
return "Invalid newsletter id %s." % newsletter_id
else:
logger.debug(u"Unable to send %snotification, no newsletter_id received." % test)
return "No newsletter id received."
@cherrypy.expose
@requireAuth(member_of("admin"))
def preview_newsletter(self, newsletter_id=None, **kwargs):
if newsletter_id:
newsletter = newsletters.get_newsletter_config(newsletter_id=newsletter_id)
newsletter_agent = newsletters.get_agent_class(agent_id=newsletter['agent_id'], config=newsletter['config'])
if newsletter_agent:
return newsletter_agent.preview()
return