mirror of
https://codeberg.org/Reuh/feather.git
synced 2025-10-27 18:19:32 +00:00
refactor: rename item -> article
This commit is contained in:
parent
07e9d208b1
commit
a587340865
6 changed files with 48 additions and 47 deletions
10
README.md
10
README.md
|
|
@ -8,19 +8,19 @@ start with pictures/gif each time
|
||||||
|
|
||||||
Directories, sorting/searching by date/title (a.k.a. using a file manager)
|
Directories, sorting/searching by date/title (a.k.a. using a file manager)
|
||||||
|
|
||||||
### Reading an item
|
### Reading an article
|
||||||
|
|
||||||
opening an item
|
opening an article
|
||||||
|
|
||||||
### Marking items as read
|
### Marking articles as read
|
||||||
|
|
||||||
Delete
|
Delete
|
||||||
|
|
||||||
See read items in the trash can
|
See read articles in the trash can
|
||||||
|
|
||||||
### Updating with the server
|
### Updating with the server
|
||||||
|
|
||||||
Call `feather sync` to synchronize all local data with the server (read items, new items from the server, etc.).
|
Call `feather sync` to synchronize all local data with the server (read articles, new articles from the server, etc.).
|
||||||
|
|
||||||
`feather daemon`
|
`feather daemon`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ def main():
|
||||||
description="file-based RSS reader client"
|
description="file-based RSS reader client"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"action", choices=("sync", "sync-up", "sync-down", "daemon", "regenerate", "clear-data"),
|
"action",
|
||||||
help="sync: perform a full synchronization with the server; sync-up: only synchronize local changes to the server (e.g. items read locally); sync-down: only synchronize remote change from the server (e.g. new items or items read from another device); daemon: start in daemon mode (will keep performing synchronizations periodically until process is stopped); regenerate: regenerate all HTML files from the local data; clear-data: remove all local data"
|
choices=("sync", "sync-up", "sync-down", "daemon", "regenerate", "clear-data"),
|
||||||
|
help="sync: perform a full synchronization with the server; sync-up: only synchronize local changes to the server (e.g. articles read locally); sync-down: only synchronize remote change from the server (e.g. new articles or articles read from another device); daemon: start in daemon mode (will keep performing synchronizations periodically until process is stopped); regenerate: regenerate all HTML files from the local data; clear-data: remove all local data",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class ClientSession(ABC):
|
||||||
config: Config
|
config: Config
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def mark_as_read(self, item_ids: list[ArticleId]):
|
def mark_as_read(self, articles_ids: list[ArticleId]):
|
||||||
"""Mark all the given articles as read."""
|
"""Mark all the given articles as read."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -41,8 +41,8 @@ class GReaderSession(ClientSession):
|
||||||
self.auth_token = self.greader.login(config.server_user, config.server_password)
|
self.auth_token = self.greader.login(config.server_user, config.server_password)
|
||||||
self.csrf_token = self.greader.get_token(self.auth_token)
|
self.csrf_token = self.greader.get_token(self.auth_token)
|
||||||
|
|
||||||
def mark_as_read(self, item_ids: list[ArticleId]):
|
def mark_as_read(self, articles_ids: list[ArticleId]):
|
||||||
self.greader.edit_tags(self.auth_token, self.csrf_token, item_ids=item_ids, add_tags=[google_reader.STREAM_READ])
|
self.greader.edit_tags(self.auth_token, self.csrf_token, item_ids=articles_ids, add_tags=[google_reader.STREAM_READ])
|
||||||
|
|
||||||
def list_categories(self) -> list[Category]:
|
def list_categories(self) -> list[Category]:
|
||||||
categories = [tag for tag in self.greader.list_tags(self.auth_token) if tag.type == "folder"]
|
categories = [tag for tag in self.greader.list_tags(self.auth_token) if tag.type == "folder"]
|
||||||
|
|
@ -95,8 +95,8 @@ class TTRSession(ClientSession):
|
||||||
self.ttrss.login()
|
self.ttrss.login()
|
||||||
self.feeds = {}
|
self.feeds = {}
|
||||||
|
|
||||||
def mark_as_read(self, item_ids: list[ArticleId]):
|
def mark_as_read(self, articles_ids: list[ArticleId]):
|
||||||
self.ttrss.mark_read(item_ids)
|
self.ttrss.mark_read(articles_ids)
|
||||||
|
|
||||||
def list_categories(self) -> list[Category]:
|
def list_categories(self) -> list[Category]:
|
||||||
self.feeds = {}
|
self.feeds = {}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ user = "username"
|
||||||
# (Required) Password/API password used to connect to the server
|
# (Required) Password/API password used to connect to the server
|
||||||
# Can be set through the environment variable SERVER_PASSWORD.
|
# Can be set through the environment variable SERVER_PASSWORD.
|
||||||
password = "password"
|
password = "password"
|
||||||
# How many items to retrieve at most from the server in a single request. Lower values will make synchronization slower, higher values might make the server complain.
|
# How many articles to retrieve at most from the server in a single request. Lower values will make synchronization slower, higher values might make the server complain.
|
||||||
# If you are missing articles after a sync, it might be because this value is too high.
|
# If you are missing articles after a sync, it might be because this value is too high.
|
||||||
# If you are using the Google Reader API: servers should be okay with up to 1000.
|
# If you are using the Google Reader API: servers should be okay with up to 1000.
|
||||||
# If you are using the ttrss API: servers should be okay with up to 200.
|
# If you are using the ttrss API: servers should be okay with up to 200.
|
||||||
|
|
@ -123,8 +123,8 @@ timezone = "Etc/UTC"
|
||||||
format = "%Y-%m-%d %H:%M"
|
format = "%Y-%m-%d %H:%M"
|
||||||
|
|
||||||
[daemon]
|
[daemon]
|
||||||
# When running in daemon mode, feather will download changes from the server (new items, items read state) every <sync_down_every> seconds.
|
# When running in daemon mode, feather will download changes from the server (new articles, articles read state) every <sync_down_every> seconds.
|
||||||
sync_down_every = 900
|
sync_down_every = 900
|
||||||
# When running in daemon mode, feather will upload local changes to the server (read items) every <sync_up_every> seconds.
|
# When running in daemon mode, feather will upload local changes to the server (read articles) every <sync_up_every> seconds.
|
||||||
sync_up_every = 60
|
sync_up_every = 60
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,12 @@ class Article(ABC):
|
||||||
|
|
||||||
def write_json(self):
|
def write_json(self):
|
||||||
stored_fields = ("id", "unread", "title", "published", "published_formatted", "updated", "updated_formatted", "author", "summary", "content", "feed_title", "feed_url", "feed_icon_url", "feed_order", "article_url", "comments_url", "language", "image_url", "html_path")
|
stored_fields = ("id", "unread", "title", "published", "published_formatted", "updated", "updated_formatted", "author", "summary", "content", "feed_title", "feed_url", "feed_icon_url", "feed_order", "article_url", "comments_url", "language", "image_url", "html_path")
|
||||||
item_json = { field: getattr(self, field) for field in stored_fields }
|
article_json = { field: getattr(self, field) for field in stored_fields }
|
||||||
item_json["category"] = self.category.asdict()
|
article_json["category"] = self.category.asdict()
|
||||||
if self.json_path.exists():
|
if self.json_path.exists():
|
||||||
raise Exception
|
raise Exception
|
||||||
with self.json_path.open("w") as f:
|
with self.json_path.open("w") as f:
|
||||||
json.dump(item_json, f)
|
json.dump(article_json, f)
|
||||||
def delete_json(self):
|
def delete_json(self):
|
||||||
self.json_path.unlink()
|
self.json_path.unlink()
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ class FileArticle(Article):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.json_path = json_path
|
self.json_path = json_path
|
||||||
|
|
||||||
item_json = json.load(json_path.open("r"))
|
article_json = json.load(json_path.open("r"))
|
||||||
for field in item_json:
|
for field in article_json:
|
||||||
setattr(self, field, item_json[field])
|
setattr(self, field, article_json[field])
|
||||||
self.category = Category.fromdict(item_json["category"])
|
self.category = Category.fromdict(article_json["category"])
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,12 @@ class FeatherApp:
|
||||||
removed_directories.add(dirpath)
|
removed_directories.add(dirpath)
|
||||||
|
|
||||||
def mark_deleted_as_read(self):
|
def mark_deleted_as_read(self):
|
||||||
"""Mark items that are in the JSON directory but with missing HTML file as read on the server"""
|
"""Mark articles that are in the JSON directory but with missing HTML file as read on the server"""
|
||||||
config = self.config
|
config = self.config
|
||||||
client_session = self.get_client_session()
|
client_session = self.get_client_session()
|
||||||
|
|
||||||
if config.update_lock.exists():
|
if config.update_lock.exists():
|
||||||
print("The previous synchronization was aborted, not marking any item as read in order to avoid collateral damage")
|
print("The previous synchronization was aborted, not marking any article as read in order to avoid collateral damage")
|
||||||
return
|
return
|
||||||
|
|
||||||
marked_as_read = 0
|
marked_as_read = 0
|
||||||
|
|
@ -67,18 +67,18 @@ class FeatherApp:
|
||||||
for i in range(0, len(to_mark_as_read), config.articles_per_query):
|
for i in range(0, len(to_mark_as_read), config.articles_per_query):
|
||||||
client_session.mark_as_read(to_mark_as_read[i:i+config.articles_per_query])
|
client_session.mark_as_read(to_mark_as_read[i:i+config.articles_per_query])
|
||||||
|
|
||||||
print(f"Marked {marked_as_read} items as read")
|
print(f"Marked {marked_as_read} articles as read")
|
||||||
|
|
||||||
def synchronize_with_server(self):
|
def synchronize_with_server(self):
|
||||||
"""Synchronize items from the server, generating and deleting JSON and HTML files accordingly"""
|
"""Synchronize articles from the server, generating and deleting JSON and HTML files accordingly"""
|
||||||
config = self.config
|
config = self.config
|
||||||
client_session = self.get_client_session()
|
client_session = self.get_client_session()
|
||||||
|
|
||||||
config.update_lock.touch()
|
config.update_lock.touch()
|
||||||
print("Synchronizing with server...")
|
print("Synchronizing with server...")
|
||||||
|
|
||||||
new_items, updated_items = 0, 0
|
new_articles, updated_articles = 0, 0
|
||||||
grabbed_item_paths = set()
|
grabbed_article_paths = set()
|
||||||
|
|
||||||
categories = client_session.list_categories()
|
categories = client_session.list_categories()
|
||||||
for category in categories:
|
for category in categories:
|
||||||
|
|
@ -92,27 +92,27 @@ class FeatherApp:
|
||||||
else:
|
else:
|
||||||
remaining = False
|
remaining = False
|
||||||
|
|
||||||
for item in articles:
|
for article in articles:
|
||||||
json_path = item.json_path
|
json_path = article.json_path
|
||||||
grabbed_item_paths.add(json_path)
|
grabbed_article_paths.add(json_path)
|
||||||
if not json_path.exists():
|
if not json_path.exists():
|
||||||
item.write()
|
article.write()
|
||||||
new_items += 1
|
new_articles += 1
|
||||||
else:
|
else:
|
||||||
old_item = FileArticle(config, json_path)
|
old_article = FileArticle(config, json_path)
|
||||||
if item.updated > old_item.updated:
|
if article.updated > old_article.updated:
|
||||||
old_item.delete()
|
old_article.delete()
|
||||||
item.write()
|
article.write()
|
||||||
updated_items += 1
|
updated_articles += 1
|
||||||
|
|
||||||
# Remove items that we didn't get from the server but are in the JSON directory
|
# Remove articles that we didn't get from the server but are in the JSON directory
|
||||||
removed_items = 0
|
removed_articles = 0
|
||||||
for item_path in config.json_root.glob("*.json"):
|
for article_path in config.json_root.glob("*.json"):
|
||||||
if not item_path in grabbed_item_paths:
|
if article_path not in grabbed_article_paths:
|
||||||
FileArticle(config, item_path).delete()
|
FileArticle(config, article_path).delete()
|
||||||
removed_items += 1
|
removed_articles += 1
|
||||||
|
|
||||||
print(f"Synchronization successful ({new_items} new items, {updated_items} updated, {removed_items} removed)")
|
print(f"Synchronization successful ({new_articles} new articles, {updated_articles} updated, {removed_articles} removed)")
|
||||||
config.update_lock.unlink()
|
config.update_lock.unlink()
|
||||||
|
|
||||||
def synchronize(self):
|
def synchronize(self):
|
||||||
|
|
@ -122,12 +122,12 @@ class FeatherApp:
|
||||||
self.remove_empty_html_directories()
|
self.remove_empty_html_directories()
|
||||||
|
|
||||||
def synchronize_local_changes(self):
|
def synchronize_local_changes(self):
|
||||||
"""Upload local changes (read items) to the server"""
|
"""Upload local changes (read articles) to the server"""
|
||||||
self.mark_deleted_as_read()
|
self.mark_deleted_as_read()
|
||||||
self.remove_empty_html_directories()
|
self.remove_empty_html_directories()
|
||||||
|
|
||||||
def synchronize_remote_changes(self):
|
def synchronize_remote_changes(self):
|
||||||
"""Download remote changes (new items, items read from another device) from the server"""
|
"""Download remote changes (new articles, articles read from another device) from the server"""
|
||||||
self.synchronize_with_server()
|
self.synchronize_with_server()
|
||||||
self.remove_empty_html_directories()
|
self.remove_empty_html_directories()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue