From b5474cb3762929f1b57948aebde51ed0031b8073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Fri, 17 Oct 2025 13:59:00 +0200 Subject: [PATCH] feat: allow marking items as unread by restoring their files and add option to not write read articles even when synced --- src/feather/app.py | 18 +++++++++++------- src/feather/config.default.toml | 9 ++++++++- src/feather/config.py | 1 + src/feather/data.py | 22 +++++++++++++--------- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/feather/app.py b/src/feather/app.py index 4db3319..27f90d7 100755 --- a/src/feather/app.py +++ b/src/feather/app.py @@ -82,13 +82,16 @@ class FeatherApp: to_mark_as_read = [] to_mark_as_unread = [] for article in self.iter_articles(): - if not article.has_html(): - if article.unread: - to_mark_as_read.append(article) - marked_as_read += 1 - else: - to_mark_as_unread.append(article) - marked_as_unread += 1 + has_html = article.has_html() + if article.unread and not has_html: + to_mark_as_read.append(article) + marked_as_read += 1 + elif not article.unread and ( + (config.write_read_articles and not has_html) + or (not config.write_read_articles and has_html) + ): + to_mark_as_unread.append(article) + marked_as_unread += 1 if len(to_mark_as_read) == len(to_mark_as_unread) == 0: return # nothing to do @@ -260,6 +263,7 @@ class FeatherApp: """Regenerate all local files using local data only""" for article in self.iter_articles(): article.regenerate() + self.remove_empty_categories() def clear_data(self): """Delete all local data""" diff --git a/src/feather/config.default.toml b/src/feather/config.default.toml index f5860c8..ce3629b 100644 --- a/src/feather/config.default.toml +++ b/src/feather/config.default.toml @@ -23,7 +23,7 @@ password = "password" # Set to 0 to let Feather choose (200 for ttrss, 1000 for googlereader). # Can be set through the environment variable SERVER_ARTICLES_PER_REQUEST. articles_per_request = 0 -# Set to true to only sync unread articles; Feather will not retrieve or store any read article. +# Set to true to only sync unread articles; Feather will not retrieve any read article from the server. # Can be set through the environment variable SERVER_ONLY_SYNC_UNREAD_ARTICLES. only_sync_unread_articles = true @@ -36,6 +36,13 @@ data = "data" reader = "reader" [html] +# If set to true, Feather will also generate articles files for read articles. +# Also remember to set server.only_sync_unread_articles = false; otherwise this will do nothing. +# The the mark-as-unread behavior will also change depending on this value: +# - if false, marking an article as unread requires its file to be recreated/restored from the trash; +# - if true, marking an article as unread requires deleting its article file (same as mark-as-read). +# Can be set through the environment variable HTML_WRITE_READ_ARTICLES. +write_read_articles = false # Template used for generating article HTML files. All templates are Jinja2 templates. # Available fields: # - id: article id (int | str) diff --git a/src/feather/config.py b/src/feather/config.py index b61508b..7f72f09 100644 --- a/src/feather/config.py +++ b/src/feather/config.py @@ -65,6 +65,7 @@ class Config: self.timezone: ZoneInfo = ZoneInfo(str(get_config("datetime", "timezone"))) self.time_format: str = str(get_config("datetime", "format")) + self.write_read_articles: bool = bool(get_config("html", "write_read_articles")) self.article_template: Template = Template( str(get_config("html", "article_template")), autoescape=True ) diff --git a/src/feather/data.py b/src/feather/data.py index 09738e4..958f45e 100644 --- a/src/feather/data.py +++ b/src/feather/data.py @@ -234,8 +234,11 @@ class Article(ABC): def _delete_html(self, missing_ok=False): """Delete the HTML file associated with this article.""" # Delete a HTML file for a JSON object - html_path = self.config.html_root / self.html_path - html_path.unlink(missing_ok=missing_ok) + if self.html_path is None: + return + else: + html_path = self.config.html_root / self.html_path + html_path.unlink(missing_ok=missing_ok) def has_html(self) -> bool: """Check if the HTML file associated with the article exists on disk.""" @@ -251,13 +254,14 @@ class Article(ABC): def write(self, recompute_paths=False): """Write all the files associated with this article to disk.""" - try: - self._write_html(recompute_path=recompute_paths) - except FileExistsError: - raise - except: - self._delete_html(missing_ok=True) - raise + if self.unread or self.config.write_read_articles: + try: + self._write_html(recompute_path=recompute_paths) + except FileExistsError: + raise + except: + self._delete_html(missing_ok=True) + raise try: self._write_json(recompute_path=recompute_paths) except FileExistsError: