Source code for flexget.plugins.output.rss
import base64
import datetime
import hashlib
import os
from loguru import logger
from sqlalchemy import Column, DateTime, Integer, String, Unicode
from flexget import db_schema, plugin
from flexget.event import event
from flexget.utils.sqlalchemy_utils import table_add_column, table_columns
from flexget.utils.template import RenderError, get_template, render_from_entry
logger = logger.bind(name='make_rss')
Base = db_schema.versioned_base('make_rss', 0)
rss2gen = True
try:
import PyRSS2Gen
except ImportError:
rss2gen = False
@db_schema.upgrade('make_rss')
def upgrade(ver, session):
if ver is None:
columns = table_columns('make_rss', session)
if 'rsslink' not in columns:
logger.info('Adding rsslink column to table make_rss.')
table_add_column('make_rss', 'rsslink', String, session)
ver = 0
return ver
[docs]
class RSSEntry(Base):
__tablename__ = 'make_rss'
id = Column(Integer, primary_key=True)
title = Column(Unicode)
description = Column(Unicode)
link = Column(String)
rsslink = Column(String)
file = Column(Unicode)
published = Column(DateTime, default=datetime.datetime.utcnow)
enc_length = Column(Integer)
enc_type = Column(String)
[docs]
class OutputRSS:
"""Write RSS containing succeeded (downloaded) entries.
Example::
make_rss: ~/public_html/flexget.rss
You may write into same file in multiple tasks.
Example::
my-task-A:
make_rss: ~/public_html/series.rss
.
.
my-task-B:
make_rss: ~/public_html/series.rss
.
.
With this example file series.rss would contain succeeded
entries from both tasks.
**Number of days / items**
By default output contains items from last 7 days. You can specify
different perioid, number of items or both. Value -1 means unlimited.
Example::
make_rss:
file: ~/public_html/series.rss
days: 2
items: 10
Generate RSS that will containing last two days and no more than 10 items.
Example 2::
make_rss:
file: ~/public_html/series.rss
days: -1
items: 50
Generate RSS that will contain last 50 items, regardless of dates.
RSS feed properties:
You can specify the URL, title, and description to include in the header
of the RSS feed.
Example::
make_rss:
file: ~/public_html/series.rss
rsslink: http://my.server.net/series.rss
rsstitle: The Flexget RSS Feed
rssdesc: Episodes about Flexget.
**RSS item title and link**
You can specify the title and link for each item in the RSS feed.
The item title can be any pattern that references fields in the input entry.
The item link can be created from one of a list of fields in the input
entry, in order of preference. The fields should be enumerated in a list.
Note that the url field is always used as last possible fallback even
without explicitly adding it into the list.
Default field list for item URL: imdb_url, input_url, url
Example::
make_rss:
file: ~/public_html/series.rss
title: '{{title}} (from {{task}})'
link:
- imdb_url
"""
schema = {
'oneOf': [
{'type': 'string'}, # TODO: path / file
{
'type': 'object',
'properties': {
'file': {'type': 'string'},
'days': {'type': 'integer'},
'items': {'type': 'integer'},
'history': {'type': 'boolean'},
'timestamp': {'type': 'boolean'},
'rsslink': {'type': 'string'},
'rsstitle': {'type': 'string'},
'rssdesc': {'type': 'string'},
'encoding': {'type': 'string'}, # TODO: only valid choices
'title': {'type': 'string'},
'template': {'type': 'string'},
'link': {'type': 'array', 'items': {'type': 'string'}},
},
'required': ['file'],
'additionalProperties': False,
},
]
}
[docs]
def on_task_output(self, task, config):
# makes this plugin count as output (stops warnings about missing outputs)
pass
[docs]
def prepare_config(self, config):
if not isinstance(config, dict):
config = {'file': config}
config.setdefault('days', 7)
config.setdefault('items', -1)
config.setdefault('history', True)
config.setdefault('encoding', 'UTF-8')
config.setdefault('timestamp', False)
config.setdefault('link', ['imdb_url', 'input_url'])
config.setdefault('title', '{{title}} (from {{task}})')
config.setdefault('template', 'rss')
# add url as last resort
config['link'].append('url')
return config
[docs]
def on_task_exit(self, task, config):
"""Store finished / downloaded entries at exit."""
if not rss2gen:
raise plugin.PluginWarning('plugin make_rss requires PyRSS2Gen library.')
config = self.prepare_config(config)
# when history is disabled, remove everything from backlog on every run (a bit hackish, rarely useful)
if not config['history']:
logger.debug('disabling history')
for item in task.session.query(RSSEntry).filter(RSSEntry.file == config['file']).all():
task.session.delete(item)
# save entries into db for RSS generation
for entry in task.accepted:
rss = RSSEntry()
try:
rss.title = entry.render(config['title'])
except RenderError as e:
logger.error(
'Error rendering jinja title for `{}` falling back to entry title: {}',
entry['title'],
e,
)
rss.title = entry['title']
for field in config['link']:
if entry.get(field) is not None:
rss.link = entry[field]
break
try:
template = get_template(config['template'], scope='task')
except ValueError as e:
raise plugin.PluginError(f'Invalid template specified: {e}')
try:
rss.description = render_from_entry(template, entry)
except RenderError as e:
logger.error(
'Error while rendering entry {}, falling back to plain title: {}', entry, e
)
rss.description = entry['title'] + ' - (Render Error)'
rss.file = config['file']
if 'rss_pubdate' in entry:
rss.published = entry['rss_pubdate']
rss.enc_length = entry.get('size', None)
rss.enc_type = entry.get('type', None)
# TODO: check if this exists and suggest disabling history if it does since it shouldn't happen normally ...
logger.debug('Saving {} into rss database', entry['title'])
task.session.add(rss)
if not rss2gen:
return
# don't generate rss when learning
if task.options.learn:
return
db_items = (
task.session
.query(RSSEntry)
.filter(RSSEntry.file == config['file'])
.order_by(RSSEntry.published.desc())
.all()
)
# make items
rss_items = []
for db_item in db_items:
add = True
if config['items'] != -1 and len(rss_items) > config['items']:
add = False
if config['days'] != -1 and (
datetime.datetime.today() - datetime.timedelta(days=config['days'])
> db_item.published
):
add = False
if add:
# add into generated feed
hasher = hashlib.sha1()
hasher.update(db_item.title.encode('utf8'))
hasher.update(db_item.description.encode('utf8'))
hasher.update(db_item.link.encode('utf8'))
guid = base64.urlsafe_b64encode(hasher.digest()).decode('ascii')
guid = PyRSS2Gen.Guid(guid, isPermaLink=False)
gen = {
'title': db_item.title,
'description': db_item.description,
'link': db_item.link,
'pubDate': db_item.published,
'guid': guid,
}
if db_item.enc_length is not None and db_item.enc_type is not None:
gen['enclosure'] = PyRSS2Gen.Enclosure(
db_item.link, db_item.enc_length, db_item.enc_type
)
logger.trace('Adding {} into rss {}', gen['title'], config['file'])
rss_items.append(PyRSS2Gen.RSSItem(**gen))
else:
# no longer needed
task.session.delete(db_item)
# make rss
rss = PyRSS2Gen.RSS2(
title=config.get('rsstitle', 'FlexGet'),
link=config.get('rsslink', 'http://flexget.com'),
description=config.get('rssdesc', 'FlexGet generated RSS feed'),
lastBuildDate=datetime.datetime.utcnow() if config['timestamp'] else None,
items=rss_items,
)
# don't run with --test
if task.options.test:
logger.info('Would write rss file with {} entries.', len(rss_items))
return
# write rss
fn = os.path.expanduser(config['file'])
with open(fn, 'wb') as file:
try:
logger.verbose('Writing output rss to {}', fn)
rss.write_xml(file, encoding=config['encoding'])
except LookupError:
logger.critical('Unknown encoding {}', config['encoding'])
return
except OSError:
# TODO: plugins cannot raise PluginWarnings in terminate event ..
logger.critical('Unable to write {}', fn)
return
[docs]
@event('plugin.register')
def register_plugin():
plugin.register(OutputRSS, 'make_rss', api_ver=2)