diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 38 | ||||
-rw-r--r-- | README.md | 56 | ||||
-rw-r--r-- | doc/fead.h2m | 34 | ||||
-rw-r--r-- | pyproject.toml | 23 | ||||
-rwxr-xr-x | src/fead.py | 36 |
6 files changed, 181 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8e9ec3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +dist/ +doc/fead.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e9ac737 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +# Makefile for fead +# Copyright (C) 2022 Nguyễn Gia Phong +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +SHELL = /bin/sh +PREFIX ?= /usr/local + +.PHONY: man all clean install uninstall + +man: src/fead.py doc/fead.h2m + help2man --name='advert generator from web feeds'\ + --include=doc/fead.h2m --output=doc/fead.1\ + --no-info src/fead.py + +all: man + +clean: + rm doc/fead.1 + +install: all + install -Dm 755 src/fead.py ${DESTDIR}${PREFIX}/bin/fead + install -Dm 644 doc/fead.1 ${DESTDIR}${PREFIX}/share/man/fead.1 + +uninstall: + rm ${DESTDIR}${PREFIX}/bin/fead + rm ${DESTDIR}${PREFIX}/share/man/fead.1 diff --git a/README.md b/README.md index ab3ff67..4109dba 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,62 @@ # fead This is a tool for advertising other blogs you like on your own -by embedding their latest post's summary generated from their web feed. +by embedding the summary of their latest post(s) extracted from their web feed. It is a rewrite of [openring] with ([rejected]) concurrency support in Python without any third-party library. +## Usage + +```console +$ fead --help +Usage: fead [OPTION]... + +Generate adverts from web feeds. + +Options: + -h, --help show this help message and exit + -v, --version show program's version number and exit + -F PATH, --feeds PATH + file containing newline-separated web feed URLs + -f URL, --feed URL addtional web feed URL (multiple use) + -n N, --count N maximum number of ads in total (default to 3) + -p N, --per-feed N maximum number of ads per feed (default to 1) + -l N, --length N maximum summary length (default to 256) + -t PATH, --template PATH + template file (default to stdin) + -o PATH, --output PATH + output file (default to stdout) + +Any use of -f before -F is ignored. +``` + +## Template format + +The template is used by Python [`str.format`][format] to generate each advert. +It can contain the following fields, delimited by braces ('{' and '}'). + +* `source_title`: title of the web feed +* `source_link`: URL to the feed's website +* `title`: title of the feed item +* `link`: URL to the item +* `time`: publication time +* `summary`: truncated content or description + +The publication time is a Python [`datetime.datetime`][datetime] object, +which supports at least C89 format codes, e.g. `{time:%Y-%m-%d}`. + +## Contributing + +Patches should be sent to [~cnx/misc@lists.sr.ht] +using [git send-email] with the following configurations: + + git config sendemail.to '~cnx/misc@lists.sr.ht' + git config format.subjectPrefix 'PATCH fead' + ## Copying +![AGPLv3](https://www.gnu.org/graphics/agplv3-155x51.png) + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either [version 3 of the License][agplv3], @@ -14,4 +64,8 @@ or (at your option) any later version. [openring]: https://sr.ht/~sircmpwn/openring [rejected]: https://lists.sr.ht/~sircmpwn/public-inbox/patches/27621 +[format]: https://docs.python.org/3/library/string.html#formatstrings +[datetime]: https://docs.python.org/3/library/datetime.html#datetime-objects +[~cnx/misc@lists.sr.ht]: https://lists.sr.ht/~cnx/misc +[git send-email]: https://git-send-email.io [agplv3]: https://www.gnu.org/licenses/agpl-3.0.html diff --git a/doc/fead.h2m b/doc/fead.h2m new file mode 100644 index 0000000..c3b19b9 --- /dev/null +++ b/doc/fead.h2m @@ -0,0 +1,34 @@ +[template] +The template is used by Python str.format to generate each advert. +It can contain the following fields, delimited by braces ('{' and '}'). + + source_title title of the web feed + source_link URL to the feed's website + title title of the feed item + link URL to the item + time publication time + summary truncated content or description + +The publication time is a Python datetime.datetime object, +which supports at least C89 format codes, e.g. {time:%Y-%m-%d}. + +[examples] +Given the these URLs in a feeds file: + + https://adol.pw/index.xml + https://cnx.gdn/feed.xml + https://xrvs.net/index.xml + +Advertisement of the two latest blogs among them, along with articles +by Drew DeVault, can be generated via the following command. + + echo "<article> + <h3><a href='{link}'>{title}</a></h3> + {summary}—<a href='{source_link}'>{source_title}</a>, {time:%F} + </article>" | fead -F feeds -f https://drewdevault.com/blog/index.xml -n 2 + +[reporting bugs] +Issues should be reported to <~cnx/misc@lists.sr.ht>. + +[see also] +python(1), strftime(3) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2d50dad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = [ "flit_core >=3.2,<4" ] +build-backend = "flit_core.buildapi" + +[project] +name = "fead" +readme = "README.md" +requires-python = ">=3.7" +license = { file = "COPYING" } +authors = [ { name = "Nguyễn Gia Phong", email = "mcsinyx@disroot.org" } ] +maintainers = [ { name = "Nguyễn Gia Phong", email = "mcsinyx@disroot.org" } ] +keywords = [ "rss", "atom" ] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Framework :: AsyncIO", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Utilities" ] +dynamic = [ "version", "description" ] +urls = { git = "https://git.sr.ht/~cnx/fead" } +scripts = { fead = "fead:main" } diff --git a/src/fead.py b/src/fead.py index ac1fcd7..ab23bd0 100755 --- a/src/fead.py +++ b/src/fead.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Generate advert from web feeds +# Advert generator from web feeds # Copyright (C) 2022 Nguyễn Gia Phong # # This program is free software: you can redistribute it and/or modify @@ -15,9 +15,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -__version__ = '0.0.1' +__doc__ = 'Advert generator from web feeds' +__version__ = '0.1.0' -from argparse import ArgumentParser, FileType +from argparse import ArgumentParser, FileType, HelpFormatter from asyncio import gather, open_connection, run from collections import namedtuple from datetime import datetime @@ -41,6 +42,18 @@ Advert = namedtuple('Advert', ('source_title', 'source_link', 'title', 'link', 'time', 'summary')) +class GNUHelpFormatter(HelpFormatter): + """Help formatter for ArgumentParser following GNU Coding Standards.""" + + def add_usage(self, usage, actions, groups, prefix='Usage: '): + """Substitute 'Usage:' for 'usage:'.""" + super().add_usage(usage, actions, groups, prefix) + + def start_section(self, heading): + """Substitute 'Options:' for 'options:'.""" + super().start_section(heading.capitalize()) + + def read_urls(path): """Read newline-separated URLs from given file path.""" return Path(path).read_text().splitlines() @@ -101,7 +114,7 @@ def parse_atom_entry(xml): rel = child.attrib.get('rel') if rel == 'alternate' or not rel: link = child.attrib['href'] elif child.tag.endswith('Atom}published'): - iso = child.text.replace('Z', '+00:00') # normalized + iso = child.text.replace('Z', '+00:00') # normalized time = datetime.fromisoformat(iso) elif child.tag.endswith('Atom}summary'): summary = child.text @@ -172,7 +185,7 @@ async def fetch_all(urls): try: return await tasks except: - tasks.cancel() # structured concurrency + tasks.cancel() # structured concurrency raise @@ -187,10 +200,14 @@ def truncate(ad, summary_length): summary_length, placeholder='…')) -if __name__ == '__main__': - parser = ArgumentParser(description='generate advert from web feeds') +def main(): + """Run command-line program.""" + parser = ArgumentParser(prog='fead', usage='%(prog)s [OPTION]...', + description='Generate adverts from web feeds.', + epilog='Any use of -f before -F is ignored.', + formatter_class=GNUHelpFormatter) parser.add_argument('-v', '--version', action='version', - version=f'fead {__version__}') + version=f'%(prog)s {__version__}') parser.add_argument('-F', '--feeds', metavar='PATH', type=read_urls, default=[], help='file containing newline-separated web feed URLs') @@ -218,3 +235,6 @@ if __name__ == '__main__': for ad in select(args.per_feed, feed))): args.output.write(template.format(**truncate(ad, args.len)._asdict())) args.output.close() + + +if __name__ == '__main__': main() |