diff --git a/cry/cli.py b/cry/cli.py index 89860fd..fef040f 100644 --- a/cry/cli.py +++ b/cry/cli.py @@ -13,7 +13,12 @@ LOG = logging.getLogger(__name__) @click.group() @click.version_option() -@click.option("-v", "--verbose", count=True) +@click.option( + "-v", + "--verbose", + count=True, + help="Increase the verbosity of the output. This option can be specified multiple times.", +) def cli(verbose): "Command line feed reader" if verbose > 1: @@ -153,3 +158,38 @@ def show(pattern, count): else: click.echo(f" ") click.echo() + + +@cli.command("list") +@click.argument("pattern", required=False, default="") +def list_feeds(pattern): + """List subscribed feeds. + + If a pattern is supplied, then filter the feeds to urls or titles that + match the pattern. Otherwise, just show everything. + """ + db = database.Database.local() + feeds = db.load_all(feed_limit=0, pattern=pattern) + + max_title = max(len(f.title) for f in feeds) + max_url = max(len(f.meta.url) for f in feeds) + + feeds.sort(key=lambda f: f.title) + + for f in feeds: + click.echo(f"{f.title:{max_title}} {f.meta.url:{max_url}}") + + +@cli.command("unsubscribe") +@click.argument("url") +def unsubscribe(url): + """Unsubscribe from the specified feed. + + (If you need to find the URL for the feed to unsubscribe from, use the + `list` command.) + """ + db = database.Database.local() + count = db.set_feed_status(url, feed.FEED_STATUS_UNSUBSCRIBED) + if count == 0: + click.echo(f"Not subscribed to feed {url}") + return 1 diff --git a/cry/database.py b/cry/database.py index 0a970cf..4bd37f6 100644 --- a/cry/database.py +++ b/cry/database.py @@ -3,6 +3,7 @@ import random import socket import sqlite3 import string +import time import typing import platformdirs @@ -33,7 +34,17 @@ SCHEMA_STATEMENTS = [ ON UPDATE CASCADE ON DELETE CASCADE ); + """, + # I went and changed the status enum to make ALIVE == 0 when I added the + # "unsubscribed" status. I should probably make these strings huh. """ + UPDATE feeds + SET status=CASE + WHEN status = 0 THEN 1 + WHEN status = 1 THEN 0 + ELSE status + END + """, ] @@ -168,8 +179,9 @@ class Database: title, link FROM feeds - WHERE title LIKE :sql_pattern ESCAPE '\\' - OR link LIKE :sql_pattern ESCAPE '\\' + WHERE (title LIKE :sql_pattern ESCAPE '\\' + OR link LIKE :sql_pattern ESCAPE '\\') + AND status != 2 -- UNSUBSCRIBED """, {"sql_pattern": sql_pattern}, ) @@ -200,21 +212,25 @@ class Database: feeds = [] for meta, title, link in almost_feeds: - cursor = self.db.execute( - """ - SELECT - id, - inserted_at, - title, - link - FROM entries - WHERE feed_url=? - ORDER BY inserted_at DESC - LIMIT ? - """, - [meta.url, feed_limit], - ) - rows = cursor.fetchall() + if feed_limit > 0: + cursor = self.db.execute( + """ + SELECT + id, + inserted_at, + title, + link + FROM entries + WHERE feed_url=? + ORDER BY inserted_at DESC + LIMIT ? + """, + [meta.url, feed_limit], + ) + rows = cursor.fetchall() + else: + rows = [] + entries = [ feed.Entry(id=id, inserted_at=inserted_at, title=title, link=link) for id, inserted_at, title, link in rows @@ -278,18 +294,26 @@ class Database: return feed.Feed(meta=meta, title=title, link=link, entries=entries) def update_meta(self, f: feed.FeedMeta): - self.db.execute( - """ - UPDATE feeds SET - last_fetched_ts=?, - retry_after_ts=?, - status=?, - etag=?, - modified=? - WHERE url=? - """, - [f.last_fetched_ts, f.retry_after_ts, f.status, f.etag, f.modified, f.url], - ) + with self.db: + self.db.execute( + """ + UPDATE feeds SET + last_fetched_ts=?, + retry_after_ts=?, + status=?, + etag=?, + modified=? + WHERE url=? + """, + [ + f.last_fetched_ts, + f.retry_after_ts, + f.status, + f.etag, + f.modified, + f.url, + ], + ) def store_feed(self, f: feed.Feed) -> int: """Store the given feed in the database. @@ -375,3 +399,16 @@ class Database: ) end_count = cursor.fetchone()[0] return end_count - start_count + + def set_feed_status(self, url: str, status: int) -> int: + with self.db: + cursor = self.db.execute( + """ + UPDATE feeds + SET status = ?, + last_fetched_ts = ? + WHERE url = ? + """, + [status, int(time.time()), url], + ) + return cursor.rowcount diff --git a/cry/feed.py b/cry/feed.py index 9e7bb60..303b363 100644 --- a/cry/feed.py +++ b/cry/feed.py @@ -18,9 +18,9 @@ import requests.structures LOG = logging.getLogger(__name__) -FEED_STATUS_DEAD = 0 -FEED_STATUS_ALIVE = 1 -FEED_STATUS_MISSING = 2 +FEED_STATUS_ALIVE = 0 +FEED_STATUS_DEAD = 1 +FEED_STATUS_UNSUBSCRIBED = 2 # TODO: Consider configuration here. http = requests.Session() @@ -143,8 +143,8 @@ async def fetch_feed( Regardless, the new FeedMeta has the latest state of the feed. """ - if feed.status == FEED_STATUS_DEAD: - LOG.info(f"{feed.url} is dead") + if feed.status != FEED_STATUS_ALIVE: + LOG.info(f"{feed.url} is dead or unsubscribed") return (None, feed) if time.time() < feed.retry_after_ts: