Zalohovani vsech podstatnych souboru
This commit is contained in:
226
skills/bookmark/scripts/bookmark.py
Normal file
226
skills/bookmark/scripts/bookmark.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bookmark skill — CRUD for reading-list entries stored in SQLite."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
DB_PATH = (
|
||||
Path(__file__).resolve().parent.parent.parent.parent / "db" / "bookmark.sqlite"
|
||||
)
|
||||
|
||||
EMPTY_TAGS_JSON = "[]"
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS bookmarks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
url TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
tags TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL,
|
||||
read_at TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def _init_db(conn: sqlite3.Connection) -> None:
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.executescript(SCHEMA)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _connect() -> sqlite3.Connection:
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
_init_db(conn)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _parse_tags(raw: str) -> list[str]:
|
||||
"""Parse comma-separated tags into a deduplicated sorted list."""
|
||||
if not raw:
|
||||
return []
|
||||
tags = [t.strip() for t in raw.split(",") if t.strip()]
|
||||
return sorted(set(tags))
|
||||
|
||||
|
||||
def _tags_display(tags_json: str) -> str:
|
||||
tags = json.loads(tags_json)
|
||||
return ", ".join(tags) if tags else ""
|
||||
|
||||
|
||||
def _domain(url: str) -> str:
|
||||
"""Extract domain from URL (strip www. prefix)."""
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
host = parsed.hostname or ""
|
||||
return host.removeprefix("www.")
|
||||
except (ValueError, AttributeError):
|
||||
return url
|
||||
|
||||
|
||||
def _print_bookmark(
|
||||
row: sqlite3.Row, *, show_status: bool = False, show_read_date: bool = False
|
||||
) -> None:
|
||||
"""Format and print a single bookmark row."""
|
||||
tags = json.loads(row["tags"])
|
||||
tag_str = f" [{', '.join(tags)}]" if tags else ""
|
||||
print(f"#{row['id']} {_domain(row['url'])}{tag_str}")
|
||||
print(f" {row['description']}")
|
||||
print(f" {row['url']}")
|
||||
line = f" added: {row['created_at'][:10]}"
|
||||
if show_status:
|
||||
status = "read" if row["read_at"] else "unread"
|
||||
line += f" status: {status}"
|
||||
if show_read_date and row["read_at"]:
|
||||
line += f" read: {row['read_at'][:10]}"
|
||||
print(line)
|
||||
|
||||
|
||||
def cmd_add(args: argparse.Namespace) -> None:
|
||||
tags = _parse_tags(args.tags)
|
||||
with _connect() as conn:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
conn.execute(
|
||||
"INSERT INTO bookmarks (url, description, tags, created_at) VALUES (?, ?, ?, ?)",
|
||||
(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}")
|
||||
|
||||
|
||||
def cmd_list(args: argparse.Namespace) -> None:
|
||||
with _connect() as conn:
|
||||
if args.tag:
|
||||
rows = conn.execute(
|
||||
"""SELECT * FROM bookmarks
|
||||
WHERE read_at IS NULL AND EXISTS (
|
||||
SELECT 1 FROM json_each(tags) WHERE json_each.value = ?
|
||||
)
|
||||
ORDER BY created_at DESC""",
|
||||
(args.tag,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM bookmarks WHERE read_at IS NULL ORDER BY created_at DESC"
|
||||
).fetchall()
|
||||
|
||||
if not rows:
|
||||
print(
|
||||
"No bookmarks." if not args.tag else f"No bookmarks with tag '{args.tag}'."
|
||||
)
|
||||
return
|
||||
|
||||
for r in rows:
|
||||
_print_bookmark(r)
|
||||
print()
|
||||
|
||||
|
||||
def cmd_read(args: argparse.Namespace) -> None:
|
||||
with _connect() as conn:
|
||||
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.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.")
|
||||
|
||||
|
||||
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
|
||||
conn.commit()
|
||||
if affected == 0:
|
||||
print(f"Bookmark #{args.id} not found or not marked as read.")
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
def cmd_history(args: argparse.Namespace) -> None:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM bookmarks WHERE read_at IS NOT NULL ORDER BY read_at DESC"
|
||||
).fetchall()
|
||||
|
||||
if not rows:
|
||||
print("No read bookmarks.")
|
||||
return
|
||||
|
||||
for r in rows:
|
||||
_print_bookmark(r, show_read_date=True)
|
||||
print()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Bookmark CRUD")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# add
|
||||
p_add = sub.add_parser("add", help="Add a bookmark")
|
||||
p_add.add_argument("url", help="URL to bookmark")
|
||||
p_add.add_argument("description", help="Short description")
|
||||
p_add.add_argument("--tags", default="", help="Comma-separated tags")
|
||||
|
||||
# list
|
||||
p_list = sub.add_parser("list", help="List unread bookmarks")
|
||||
p_list.add_argument("--tag", help="Filter by tag (exact match)")
|
||||
|
||||
# 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")
|
||||
|
||||
# unread (unmark)
|
||||
p_unread = sub.add_parser("unread", help="Unmark bookmark as read")
|
||||
p_unread.add_argument("id", type=int, help="Bookmark ID")
|
||||
|
||||
# show (display details)
|
||||
p_show = sub.add_parser("show", help="Show bookmark details")
|
||||
p_show.add_argument("id", type=int, help="Bookmark ID")
|
||||
|
||||
# history (list read)
|
||||
sub.add_parser("history", help="List read bookmarks")
|
||||
|
||||
dispatch = {
|
||||
"add": cmd_add,
|
||||
"list": cmd_list,
|
||||
"read": cmd_read,
|
||||
"unread": cmd_unread,
|
||||
"show": cmd_show,
|
||||
"history": cmd_history,
|
||||
}
|
||||
args = parser.parse_args()
|
||||
dispatch[args.command](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user