provozni zaloha

This commit is contained in:
lachtan
2026-06-24 08:11:12 +02:00
parent 9295dba19f
commit 1db3ec4756
97 changed files with 7698 additions and 817 deletions

View File

@@ -44,6 +44,33 @@ def _connect() -> sqlite3.Connection:
conn.close()
def _ordered_ids(conn: sqlite3.Connection, *, read: bool) -> list[int]:
"""Internal ids of one bookmark set in display order.
Unread (`read=False`) is what `list` shows, read (`read=True`) what `history`
shows. Display IDs are 1-based positions here, computed on the fly — never
stored — so they renumber whenever the set changes.
"""
if read:
rows = conn.execute(
"SELECT id FROM bookmarks WHERE read_at IS NOT NULL ORDER BY read_at DESC"
).fetchall()
else:
rows = conn.execute(
"SELECT id FROM bookmarks WHERE read_at IS NULL ORDER BY created_at DESC"
).fetchall()
return [row["id"] for row in rows]
def _resolve_display_id(conn: sqlite3.Connection, display_id: int, *, read: bool) -> int | None:
"""Translate a display ID into an internal id, or None if out of range."""
order = _ordered_ids(conn, read=read)
idx = display_id - 1
if idx < 0 or idx >= len(order):
return None
return order[idx]
def _parse_tags(raw: str) -> list[str]:
"""Parse comma-separated tags into a deduplicated sorted list."""
if not raw:
@@ -68,12 +95,12 @@ def _domain(url: str) -> str:
def _print_bookmark(
row: sqlite3.Row, *, show_status: bool = False, show_read_date: bool = False
row: sqlite3.Row, display_id: int, *, show_status: bool = False, show_read_date: bool = False
) -> None:
"""Format and print a single bookmark row."""
"""Format and print a single bookmark row under its display ID."""
tags = json.loads(row["tags"])
tag_str = f" [{', '.join(tags)}]" if tags else ""
print(f"#{row['id']} {_domain(row['url'])}{tag_str}")
print(f"#{display_id} {_domain(row['url'])}{tag_str}")
print(f" {row['description']}")
print(f" {row['url']}")
line = f" added: {row['created_at'][:10]}"
@@ -94,13 +121,14 @@ def cmd_add(args: argparse.Namespace) -> None:
(args.url, args.description, json.dumps(tags, ensure_ascii=False), now),
)
conn.commit()
bid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
tag_info = f" [{', '.join(tags)}]" if tags else ""
print(f"Added bookmark #{bid}: {args.url}{tag_info}")
# Newest unread sorts first, so a fresh bookmark is always display #1.
print(f"Added bookmark #1: {args.url}{tag_info}")
def cmd_list(args: argparse.Namespace) -> None:
with _connect() as conn:
display_by_id = {nid: i + 1 for i, nid in enumerate(_ordered_ids(conn, read=False))}
if args.tag:
rows = conn.execute(
"""SELECT * FROM bookmarks
@@ -121,49 +149,44 @@ def cmd_list(args: argparse.Namespace) -> None:
)
return
# Display IDs come from the full unread set so a tag-filtered list keeps the
# same numbers `read`/`show` resolve against (gaps are expected when filtered).
for r in rows:
_print_bookmark(r)
_print_bookmark(r, display_by_id[r["id"]])
print()
def cmd_read(args: argparse.Namespace) -> None:
with _connect() as conn:
internal_id = _resolve_display_id(conn, args.id, read=False)
if internal_id is None:
print(f"No unread bookmark #{args.id}.")
return
now = datetime.now(timezone.utc).isoformat()
cur = conn.execute(
"UPDATE bookmarks SET read_at = ? WHERE id = ? AND read_at IS NULL",
(now, args.id),
)
affected = cur.rowcount
conn.execute("UPDATE bookmarks SET read_at = ? WHERE id = ?", (now, internal_id))
conn.commit()
if affected == 0:
print(f"Bookmark #{args.id} not found or already marked as read.")
else:
print(f"Marked bookmark #{args.id} as read.")
print(f"Marked bookmark #{args.id} as read.")
def cmd_unread(args: argparse.Namespace) -> None:
with _connect() as conn:
cur = conn.execute(
"UPDATE bookmarks SET read_at = NULL WHERE id = ? AND read_at IS NOT NULL",
(args.id,),
)
affected = cur.rowcount
internal_id = _resolve_display_id(conn, args.id, read=True)
if internal_id is None:
print(f"No read bookmark #{args.id} in history.")
return
conn.execute("UPDATE bookmarks SET read_at = NULL WHERE id = ?", (internal_id,))
conn.commit()
if affected == 0:
print(f"Bookmark #{args.id} not found or not marked as read.")
else:
print(f"Unmarked bookmark #{args.id}.")
print(f"Unmarked bookmark #{args.id}.")
def cmd_show(args: argparse.Namespace) -> None:
with _connect() as conn:
row = conn.execute(
"SELECT * FROM bookmarks WHERE id = ?", (args.id,)
).fetchone()
if not row:
print(f"Bookmark #{args.id} not found.")
return
_print_bookmark(row, show_status=True)
internal_id = _resolve_display_id(conn, args.id, read=False)
if internal_id is None:
print(f"No unread bookmark #{args.id}.")
return
row = conn.execute("SELECT * FROM bookmarks WHERE id = ?", (internal_id,)).fetchone()
_print_bookmark(row, args.id, show_status=True)
def cmd_history(args: argparse.Namespace) -> None:
@@ -176,8 +199,8 @@ def cmd_history(args: argparse.Namespace) -> None:
print("No read bookmarks.")
return
for r in rows:
_print_bookmark(r, show_read_date=True)
for display_id, r in enumerate(rows, start=1):
_print_bookmark(r, display_id, show_read_date=True)
print()
@@ -197,15 +220,15 @@ def main() -> None:
# read (mark as read)
p_read = sub.add_parser("read", help="Mark bookmark as read")
p_read.add_argument("id", type=int, help="Bookmark ID")
p_read.add_argument("id", type=int, help="Display ID from `list`")
# unread (unmark)
p_unread = sub.add_parser("unread", help="Unmark bookmark as read")
p_unread.add_argument("id", type=int, help="Bookmark ID")
p_unread.add_argument("id", type=int, help="Display ID from `history`")
# show (display details)
p_show = sub.add_parser("show", help="Show bookmark details")
p_show.add_argument("id", type=int, help="Bookmark ID")
p_show.add_argument("id", type=int, help="Display ID from `list`")
# history (list read)
sub.add_parser("history", help="List read bookmarks")