April 13, 2020

TTRSS + Calibre - Turn your RSS feed into an EBook

After turning an old Android tablet into a dedicated E-Reader by utilising KOReader, I also turned to Calibre to manage my E-Books. Utilising this Caibre recipe in this gist by oott123 I was able to get Calibre to pull my RSS feed (hosted on my TTRSS raspberry pi) into a ebook, that I could sync with my “reader” and read on the go.

Create a new “news” source

  1. From the “Fetch News” icon in the main tool bar of Calibre, select the down arrow and click “Add or edit a custom news source”

Adding custom news source

  1. Create a new recipe by clicking the “New recipe” button down the bottom left area of the pop up window

Add a new recipe

  1. Give it a name and fill out the form to your prefernce, and then switch over to advanced mode

Switch to advanced mode

  1. Copy in the recipe and adjust the variables in the code to point to your TTRSS instance. Once done click save

Code and stuff

  1. From here you can add automatic download schedule that Calibre will handle (usefull if you run calibre on a server somewhere) and hit Okay

    Adjust scheduling

  2. Sync your device with Calibre and it will bring your feeds in. This works nicely if you fetch it daily, as you will then have a daily news book to read. This recipe also marks the fetched news items as read within TTRSS

How Calibre shows a ttrss ebook in the list


#!/usr/bin/env python2

from __future__ import unicode_literals, division, absolute_import, print_function
from calibre.web.feeds.news import BasicNewsRecipe
import json, urllib2, urllib, pprint, re, ssl

class TClient():
    def __init__(self, url, username, password, logger, key, test = False):
        self.url = "%s/api/" % url
        self.username = username
        self.password = password
        self.log = logger
        self.sid = None
        self.test = test
        self.key = key
    def _request(self, op, data = None):
        if not op is 'login':
            data['sid'] = self.sid
        if data is None:
            data = {}
        data['op'] = op
        data_string = json.dumps(data)
        json_string = None
            req = urllib2.Request(self.url, data_string, {'User-Agent': 'Mozilla/4.0'})
            res = urllib2.urlopen(req, context=ssl._create_unverified_context())
            json_string = res.read()
        except urllib2.HTTPError,e:
            raise e
        object = json.loads(json_string)
        if 'error' in object:
            raise Exception("Tiny Tiny RSS Error: URL-%s, DATA-%s, RESP-%s" %(self.url, data_string, json_string))
        return object['content']
    def login(self):
        data = self._request('login', {
            "user": self.username,
            "password": self.password
        if not 'session_id' in data:
            self.log.warn("Tiny Tiny RSS Error: failed to load session id.")
            raise Exception("Tiny Tiny RSS Error: failed to load session id.")
        self.sid = data['session_id']
        self.log.info("Get session id %s" % self.sid)
    def get_articles(self, urls, offset = 0, limit = 10):
        id_list = []
        data = {}
        data['feed_id'] = -4 # all unread

        data['limit'] = limit
        data['offset'] = offset
        data['show_content'] = True
        data['view_mode'] = 'unread'
        data['sanitize'] = False
        feeds = self._request('getHeadlines', data)
        for i in feeds:
            if not i['feed_title'] in urls:
                urls[i['feed_title']] = []
                'title': i['title'],
                'url': i['link'],
                'date': i['updated'],
                #'content': self.append_url(i['content'], i['link']),

                'content': i['content'],
                'description': i['excerpt'] if 'excerpt' in i else ''
            id_list.append("%s" % i['id'])
        read_data = {
            "article_ids": ",".join(id_list),
            "mode": 0,
            "field": 2
        if not self.test:
            self._request('updateArticle', read_data)
        return urls
    def get_all_articles(self):
        urls = {}
        countLast = 0
        while True:
            counters = self._request('getCounters', {'mode': 'f'})
            count = 0
            for i in counters:
                if i['id'] is -4:
                        count = i['counter']
            if count < 1:
            if countLast is count:
                raise Exception("There's some error when marking read articles.")
            countLast = count
            urls = self.get_articles(urls)
            if self.test:
        return urls
    def append_url(self, raw_html, url):
        u = urllib.quote(url)
        share_url = "http://httpbin.org/get?key=%s&url=%s" % (self.key, u)
        qr_url = "https://chart.googleapis.com/chart?cht=qr&chs=300x300&choe=UTF-8&chld=H|4&chl=%s" % u
        append_html = u' ' % (qr_url, share_url, url)
        raw_html = re.sub(r'(\</body\>|$)', r'%s\1' % append_html, raw_html, count=1)
        return raw_html

class YueDu(BasicNewsRecipe):
    title = u'TTRSS + Calibre'
    __author__ = 'Mrwhale'
    description = u'TTRSS + Calibre'
    timefmt = '%Y-%m-%d %A'
    needs_subscription = True
    oldest_article = 256
    max_articles_per_feed = 256
    publication_type = 'newspaper'
    compress_news_images = True
    use_embedded_content = True
    cover_url = ''
    masthead_url = ''
    def parse_index(self):
        ttrss_url = "URL"
        ttrss_username = "user"
        ttrss_password = "pass"
        ttrss_key = "WTF_IS_OK"
        #(ttrss_url, ttrss_username, ttrss_key) = self.username.split(';')

        self.log.info("Getting URL-%s, username-%s" % (ttrss_url, ttrss_username))
        ttrss_password = self.password
        self.log.info("Getting Password-%s******" % ttrss_password[:2])
        ttrss = TClient(ttrss_url, ttrss_username, ttrss_password, self.log, ttrss_key, self.test)
        urls = ttrss.get_all_articles()
        index = []
        for (key, value) in urls.iteritems():
            self.log.info("Key:%s - length: %d" % (key, len(value)))
            index.append((key, value))
        return index