summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile38
-rw-r--r--README.md56
-rw-r--r--doc/fead.h2m34
-rw-r--r--pyproject.toml23
-rwxr-xr-xsrc/fead.py36
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}&mdash;<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()