summaryrefslogblamecommitdiffstats
path: root/.bin/nwsflux
blob: 3e725b01d900bc2b57adfb77cd2f25b1de418d55 (plain) (tree)































































































































































































                                                                                                           
#!/usr/bin/env python3
#
# nwsflux

import miniflux
import urllib3
import logging
import os
import re
import shlex
import argparse


def get_local_feed(line: str) -> dict:
    """
    Parse a line and return a constructed dict like miniflux's API.
    """
    feed_url = shlex.split(line)[0]
    title = shlex.split(line)[-1]
    tag = shlex.split(line)[1]
    if tag == title:
        tag = 'all'

    return {
        'feed_url': feed_url,
        'title': title,
        'category': {
            'title': tag
        }
    }


def get_local_feeds(filename: str) -> list:
    """
    Read feeds from a newsboat url file.
    """

    with open(filename, 'r') as f:
        content = f.readlines()

    r = re.compile('^http.*://')
    content = list(filter(r.match, content))

    return [get_local_feed(line) for line in content]


def delete_categories(client: miniflux.Client, local_cats: dict, remote_cats: dict):
    """
    Remove remote categories that are absent in local file.

    . Useless for now, miniflux's API return non-empty categories only!
    """
    for remote in remote_cats:
        if any(local['title'] == remote['title'] for local in local_cats):
            continue
        try:
            logging.info(f'remove category: {remote["title"]}')
            client.delete_category(remote['id'])
        # ignores categories that are empty on remote
        except miniflux.ClientError as e:
            logging.error('can not remove non-empty category:'
                          f'{remote["title"]} {e}')


def create_categories(client: miniflux.Client, local_cats: dict, remote_cats: dict):
    """
    Create categories present in local file and absent on remote.
    """
    for local in local_cats:
        if any(remote['title'] == local['title'] for remote in remote_cats):
            continue
        try:
            logging.info(f'create category: {local["title"]}')
            client.create_category(local['title'])
        # ignores categories that are empty on remote
        except miniflux.ClientError as e:
            logging.error(f'remote category empty: {local["title"]} {e}')


def sync_categories(client: miniflux.Client, local_feeds: dict, remote_feeds: dict) -> dict:
    local_cats = get_uniq_categories(local_feeds)
    remote_cats = get_uniq_categories(remote_feeds)

    delete_categories(client, local_cats, remote_cats)
    create_categories(client, local_cats, remote_cats)
    return client.get_categories()


def get_uniq_categories(buffer: list) -> list:
    return list(map(dict, frozenset(
        frozenset(x['category'].items()) for x in buffer
    )))


def delete_feeds(client: miniflux.Client, local_feeds: dict, remote_feeds: dict):
    """
    Remove remote feeds that are absent in local file.
    """
    for remote in remote_feeds:
        if any(local['feed_url'] == remote['feed_url'] for local in local_feeds):
            continue
        logging.info(f'remove feed: {remote["feed_url"]}')
        client.delete_feed(remote['id'])


def create_feeds(client: miniflux.Client, local_feeds: dict, remote_feeds: dict):
    """
    Create feeds that are present in local file and absent on remote.
    """
    categories = client.get_categories()

    for local in local_feeds:
        if any(remote['feed_url'] == local['feed_url'] for remote in remote_feeds):
            continue
        logging.info(f'create feed: {local}')
        category_id = next((x['id'] for x in categories if x['title'] == local['category']['title']), None)
        try:
            client.create_feed(local['feed_url'], category_id)
        except miniflux.ClientError as e:
            logging.error(e)


def sync_feeds(client: miniflux.Client, local_feeds: dict, remote_feeds: dict) -> dict:
    """
    Synchronize all given feeds.
    """
    delete_feeds(client, local_feeds, remote_feeds)
    create_feeds(client, local_feeds, remote_feeds)
    return client.get_feeds()


def parse():
    """
    Parse command-line arguments.
    """
    parser = argparse.ArgumentParser(description='Synchronize RSS feeds from'
                                     'newsboat to an miniflux instance.')

    parser.add_argument('-c', dest='config', type=str, required=True,
                        help='Newsboat url file')
    parser.add_argument('-k', dest='key', type=str, required=True,
                        help='Miniflux API key')
    parser.add_argument('-u', dest='url', type=str, required=True,
                        help='Miniflux url')
    parser.add_argument('-d', dest='debug', action='store_true',
                        help='Enable debugging output')
    parser.add_argument('-e', dest='export', action='store_true',
                        help='Export Miniflux OPML config to stdout')

    return parser.parse_args()


def main():

    """
    SSL verify.
    """
    os.environ["CURL_CA_BUNDLE"] = ""
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    """
    Arguments.
    """
    args = parse()

    """
    Debugging.
    """
    logging.basicConfig(level=logging.INFO)
    if not args.debug:
        logging.level = logging.NOTSET

    """
    Synchronization.
    """
    client = miniflux.Client(args.url, api_key=args.key)

    local_feeds = get_local_feeds(args.config)
    remote_feeds = client.get_feeds()

    sync_categories(client, local_feeds, remote_feeds)
    sync_feeds(client, local_feeds, remote_feeds)

    """
    Export Miniflux to OPML.
    """
    if args.export:
        print(client.export_feeds())


if __name__ == '__main__':
    main()
remember that computers suck.