db.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python3
  2. # vim: tabstop=4 shiftwidth=4 expandtab
  3. import sqlite3
  4. from collections.abc import Iterable
  5. from contextlib import contextmanager
  6. from gemini_antenna.multiFeedParsing import FeedEntry, TwtxtEntry
  7. # There's a lot of opening and closing going on here,
  8. # because several processes will be sharing one sqlite3
  9. # db, which is just a single file. We want to hog it
  10. # as little as possible to minimize the risk of
  11. # collisions. Some errors are tolerable; this is a
  12. # good enough effort.
  13. class AntennaDB:
  14. def __init__(self, dbPath="antenna.sqlite"):
  15. self.dbPath = dbPath
  16. @contextmanager
  17. def cursor(self, readonly=False) -> sqlite3.Cursor:
  18. connection = sqlite3.connect(self.dbPath)
  19. cursor = connection.cursor()
  20. yield cursor
  21. if not readonly:
  22. connection.commit()
  23. connection.close()
  24. def createDB(self) -> None:
  25. """ Initialize the database. """
  26. with self.cursor() as cursor:
  27. cursor.execute("CREATE TABLE IF NOT EXISTS feedqueue (url text)")
  28. cursor.execute(
  29. "CREATE TABLE IF NOT EXISTS entries "
  30. "(feedurl text, author text, updated datetime, title text,"
  31. " link text primary key)"
  32. )
  33. cursor.execute(
  34. "CREATE TABLE IF NOT EXISTS twtxt "
  35. "(feedurl text, author text, posted datetime, twt text)"
  36. )
  37. def queueFeed(self, *urls: str) -> None:
  38. """ Add URLs to the feed queue. """
  39. urlTuples = [(url,) for url in urls]
  40. with self.cursor() as cursor:
  41. cursor.executemany("INSERT INTO feedqueue (url) VALUES (?)",
  42. urlTuples)
  43. def getQueue(self) -> list[str]:
  44. """
  45. :returns: List of URLs in the feed queue.
  46. """
  47. with self.cursor(readonly=True) as cursor:
  48. cursor.execute("SELECT * FROM feedqueue")
  49. results = []
  50. for result in cursor.fetchall():
  51. results.append(result[0])
  52. return results
  53. def getEntries(self) -> list[FeedEntry]:
  54. with self.cursor(readonly=True) as cursor:
  55. cursor.execute(
  56. "SELECT feedurl, author, updated, title, link FROM entries "
  57. "ORDER BY updated DESC"
  58. )
  59. results = []
  60. for result in cursor.fetchall():
  61. results.append(FeedEntry(feedurl = result[0],
  62. author = result[1],
  63. updated = result[2],
  64. title = result[3],
  65. link = result[4]))
  66. return results
  67. def getTwts(self) -> list[TwtxtEntry]:
  68. with self.cursor(readonly=True) as cursor:
  69. cursor.execute(
  70. "SELECT feedurl, author, posted, twt FROM twtxt "
  71. "ORDER BY posted DESC"
  72. )
  73. results = []
  74. for result in cursor.fetchall():
  75. results.append(TwtxtEntry(feedurl = result[0],
  76. author = result[1],
  77. posted = result[2],
  78. twt = result[3]))
  79. return results
  80. def deleteFeeds(self, *urls: str):
  81. """ DELETE entries with matching feed URLs from the database. """
  82. urlTuples = [(url,) for url in urls]
  83. with self.cursor() as cursor:
  84. cursor.executemany("DELETE FROM entries WHERE feedurl LIKE ?",
  85. urlTuples)
  86. cursor.executemany("DELETE FROM twtxt WHERE feedurl LIKE ?",
  87. urlTuples)
  88. def deleteFromQueue(self, *urls: str):
  89. """ DELETE feed URLs from feedqueue. """
  90. urlTuples = [(url,) for url in urls]
  91. with self.cursor() as cursor:
  92. cursor.executemany("DELETE FROM feedqueue WHERE url LIKE ?",
  93. urlTuples)
  94. def insertFeedEntries(self, entries: Iterable[FeedEntry], limit: int = 0) -> int:
  95. """
  96. INSERT entries into the DB, if they're not too old.
  97. :param limit: Maximum age timestamp.
  98. :returns: The number of entries inserted.
  99. """
  100. entries = [e for e in entries if e.updated > limit]
  101. entrytuples = []
  102. for entry in entries:
  103. entrytuples.append((entry.feedurl, entry.author, entry.updated,
  104. entry.title, entry.link))
  105. with self.cursor() as cursor:
  106. cursor.executemany(
  107. "INSERT INTO entries (feedurl, author, updated, title, link) "
  108. "VALUES (?,?,?,?,?) ON CONFLICT (link) DO UPDATE SET "
  109. "author = excluded.author, updated = excluded.updated, title = excluded.title",
  110. entrytuples
  111. )
  112. return len(entries)
  113. def insertTwtxtEntries(self, entries: Iterable[TwtxtEntry], limit: int = 0) -> int:
  114. """
  115. INSERT entries into the DB, if they're not too old.
  116. :param limit: Maximum age timestamp.
  117. :returns: The number of entries inserted.
  118. """
  119. entries = [e for e in entries if e.posted > limit]
  120. entrytuples = []
  121. for entry in entries:
  122. entrytuples.append((entry.feedurl, entry.author, entry.posted, entry.twt))
  123. with self.cursor() as cursor:
  124. cursor.executemany(
  125. "INSERT INTO twtxt (feedurl, author, posted, twt) "
  126. "VALUES (?,?,?,?)", entrytuples
  127. )
  128. return len(entries)
  129. def pruneDB(self, limit: int) -> None:
  130. """
  131. DELETE old articles from the database.
  132. :param limit: Maximum age timestamp.
  133. """
  134. with self.cursor() as cursor:
  135. cursor.execute("DELETE FROM entries WHERE updated < ?", (limit,))
  136. cursor.execute("DELETE FROM twtxt WHERE posted < ?", (limit,))