Slixfeed/slixfeed/sqlitehandler.py
2023-12-01 13:22:03 +00:00

1549 lines
41 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TODO
1) Table feeds:
category
type (atom, rdf, rss0.9. rss2 etc.)
2) Function mark_all_read for entries of given feed
3) Statistics
"""
import sqlite3
import asyncio
from bs4 import BeautifulSoup
from datetime import date
from sqlite3 import Error
import confighandler
import datahandler
import datetimehandler
# from eliot import start_action, to_file
# # with start_action(action_type="list_feeds()", db=db_file):
# # with start_action(action_type="last_entries()", num=num):
# # with start_action(action_type="get_feeds()"):
# # with start_action(action_type="remove_entry()", source=source):
# # with start_action(action_type="search_entries()", query=query):
# # with start_action(action_type="check_entry()", link=link):
# aiosqlite
DBLOCK = asyncio.Lock()
CURSORS = {}
def create_connection(db_file):
"""
Create a database connection to the SQLite database
specified by db_file.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
conn : object
Connection object or None.
"""
conn = None
try:
conn = sqlite3.connect(db_file)
return conn
except Error as e:
print(e)
return conn
def create_tables(db_file):
"""
Create SQLite tables.
Parameters
----------
db_file : str
Path to database file.
"""
with create_connection(db_file) as conn:
feeds_table_sql =(
"CREATE TABLE IF NOT EXISTS feeds ("
"id INTEGER PRIMARY KEY,"
"name TEXT,"
"address TEXT NOT NULL,"
"enabled INTEGER NOT NULL,"
"scanned TEXT,"
"updated TEXT,"
"status INTEGER,"
"valid INTEGER"
");"
)
entries_table_sql = (
"CREATE TABLE IF NOT EXISTS entries ("
"id INTEGER PRIMARY KEY,"
"title TEXT NOT NULL,"
"summary TEXT NOT NULL,"
"link TEXT NOT NULL,"
"entry_id TEXT,"
"source TEXT NOT NULL,"
"timestamp TEXT,"
"read INTEGER"
");"
)
archive_table_sql = (
"CREATE TABLE IF NOT EXISTS archive ("
"id INTEGER PRIMARY KEY,"
"title TEXT NOT NULL,"
"summary TEXT NOT NULL,"
"link TEXT NOT NULL,"
"entry_id TEXT,"
"source TEXT NOT NULL,"
"timestamp TEXT,"
"read INTEGER"
");"
)
# statistics_table_sql = (
# "CREATE TABLE IF NOT EXISTS statistics ("
# "id INTEGER PRIMARY KEY,"
# "title TEXT NOT NULL,"
# "number INTEGER"
# ");"
# )
settings_table_sql = (
"CREATE TABLE IF NOT EXISTS settings ("
"id INTEGER PRIMARY KEY,"
"key TEXT NOT NULL,"
"value INTEGER"
");"
)
cur = conn.cursor()
# cur = get_cursor(db_file)
cur.execute(feeds_table_sql)
cur.execute(entries_table_sql)
cur.execute(archive_table_sql)
# cur.execute(statistics_table_sql)
cur.execute(settings_table_sql)
def get_cursor(db_file):
"""
Allocate a cursor to connection per database.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
CURSORS[db_file] : object
Cursor.
"""
if db_file in CURSORS:
return CURSORS[db_file]
else:
with create_connection(db_file) as conn:
cur = conn.cursor()
CURSORS[db_file] = cur
return CURSORS[db_file]
async def add_feed(db_file, url, title=None, status=None):
"""
Add a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
url : str
URL.
title : str, optional
Feed Title. The default is None.
status : str, optional
HTTP status code. The default is None.
Returns
-------
msg : str
Message.
"""
#TODO consider async with DBLOCK
#conn = create_connection(db_file)
# with create_connection(db_file) as conn:
# #exist = await check_feed_exist(conn, url)
# exist = await check_feed_exist(db_file, url)
# if not exist:
# status = await main.download_feed(url)
# else:
# return "News source is already listed in the subscription list"
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
# title = feed["feed"]["title"]
feed = (title, url, 1, status, 1)
sql = (
"INSERT INTO feeds("
"name, address, enabled, status, valid"
")"
"VALUES(?, ?, ?, ?, ?) "
)
cur.execute(sql, feed)
source = title if title else '<' + url + '>'
msg = (
"> {}\nNews source \"{}\" has been added "
"to subscription list."
).format(url, source)
return msg
async def remove_feed(db_file, ix):
"""
Delete a feed by feed ID.
Parameters
----------
db_file : str
Path to database file.
ix : str
Index of feed.
Returns
-------
msg : str
Message.
"""
with create_connection(db_file) as conn:
async with DBLOCK:
cur = conn.cursor()
try:
sql = (
"SELECT address "
"FROM feeds "
"WHERE id = ?"
)
# cur
# for i in url:
# url = i[0]
url = cur.execute(sql, (ix,)).fetchone()[0]
sql = (
"SELECT name "
"FROM feeds "
"WHERE id = ?"
)
name = cur.execute(sql, (ix,)).fetchone()[0]
# NOTE Should we move DBLOCK to this line? 2022-12-23
sql = (
"DELETE "
"FROM entries "
"WHERE source = ?"
)
cur.execute(sql, (url,))
sql = (
"DELETE "
"FROM archive "
"WHERE source = ?"
)
cur.execute(sql, (url,))
sql = (
"DELETE FROM feeds "
"WHERE id = ?"
)
cur.execute(sql, (ix,))
msg = (
"> {}\nNews source \"{}\" has been removed "
"from subscription list."
).format(url, name)
except:
msg = (
"No news source with ID {}."
).format(ix)
return msg
async def check_feed_exist(db_file, url):
"""
Check whether a feed exists.
Query for feeds by given url.
Parameters
----------
db_file : str
Path to database file.
url : str
URL.
Returns
-------
result : list
List of ID and Name of feed.
"""
cur = get_cursor(db_file)
sql = (
"SELECT id, name "
"FROM feeds "
"WHERE address = ?"
)
result = cur.execute(sql, (url,)).fetchone()
return result
async def get_number_of_items(db_file, table):
"""
Return number of entries or feeds.
Parameters
----------
db_file : str
Path to database file.
table : str
"entries" or "feeds".
Returns
-------
count : ?
Number of rows.
"""
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT count(id) "
"FROM {}"
).format(table)
count = cur.execute(sql).fetchone()[0]
return count
async def get_number_of_feeds_active(db_file):
"""
Return number of active feeds.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
count : ?
Number of rows.
"""
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT count(id) "
"FROM feeds "
"WHERE enabled = 1"
)
count = cur.execute(sql).fetchone()[0]
return count
async def get_number_of_entries_unread(db_file):
"""
Return number of unread items.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
count : ?
Number of rows.
"""
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT "
"("
"SELECT count(id) "
"FROM entries "
"WHERE read = 0"
") "
"+ "
"("
"SELECT count(id) "
"FROM archive"
") "
"AS total_count"
)
count = cur.execute(sql).fetchone()[0]
return count
# TODO Read from entries and archives
async def get_entry_unread(db_file, num=None):
"""
Extract information from unread entries.
Parameters
----------
db_file : str
Path to database file.
num : str, optional
Number. The default is None.
Returns
-------
entry : str
News item message.
"""
if not num:
num = await get_settings_value(db_file, "quantum")
else:
num = int(num)
with create_connection(db_file) as conn:
cur = conn.cursor()
# sql = (
# "SELECT id "
# "FROM entries "
# "WHERE read = 0 "
# "LIMIT 1"
# )
# sql = ("SELECT id "
# "FROM entries "
# "WHERE read = 0 "
# "ORDER BY timestamp DESC "
# "LIMIT 1"
# )
# sql = (
# "SELECT id, title, summary, link "
# "FROM entries "
# "WHERE read = 0 "
# "ORDER BY timestamp "
# "DESC LIMIT :num"
# )
sql = (
"SELECT id, title, summary, link, source, timestamp "
"FROM entries "
"WHERE read = 0 "
"UNION ALL "
"SELECT id, title, summary, link, source, timestamp "
"FROM archive "
"ORDER BY timestamp "
"DESC LIMIT :num"
)
results = cur.execute(sql, (num,))
results = results.fetchall()
# TODO Add filtering
# TODO Do this when entry is added to list and mark it as read
# DONE!
# results = []
# if get_settings_value(db_file, "filter-deny"):
# while len(results) < num:
# result = cur.execute(sql).fetchone()
# blacklist = await get_settings_value(db_file, "filter-deny").split(",")
# for i in blacklist:
# if i in result[1]:
# continue
# print("rejected:", result[1])
# print("accepted:", result[1])
# results.extend([result])
# news_list = "You've got {} news items:\n".format(num)
news_list = ""
# NOTE Why doesn't this work without list?
# i.e. for result in results
# for result in results.fetchall():
for result in results:
ix = result[0]
title = result[1]
summary = result[2]
# Remove HTML tags
summary = BeautifulSoup(summary, "lxml").text
# TODO Limit text length
summary = summary.replace("\n\n\n", "\n\n")
length = await get_settings_value(db_file, "length")
summary = summary[:length] + " […]"
summary = summary.strip().split('\n')
summary = ["> " + line for line in summary]
summary = "\n".join(summary)
link = result[3]
sql = (
"SELECT name "
"FROM feeds "
"WHERE address = :source "
)
source = result[4]
feed = cur.execute(sql, (source,))
feed = feed.fetchone()[0]
if num > 1:
news_list += (
"\n{}\n{}\n"
).format(
str(title),
str(link)
)
else:
news_list = (
"{}\n\n{}\n\n{}\n{}"
).format(
str(title),
str(summary),
str(link),
str(feed)
)
# TODO While `async with DBLOCK` does work well from
# outside of functions, it would be better practice
# to place it within the functions.
async with DBLOCK:
# NOTE: We can use DBLOCK once for both
# functions, because, due to exclusive
# ID, only one can ever occur.
await mark_entry_as_read(cur, ix)
await delete_entry(cur, ix)
return news_list
async def mark_entry_as_read(cur, ix):
"""
Set read status of entry as read.
Parameters
----------
cur : object
Cursor object.
ix : str
Index of entry.
"""
sql = (
"UPDATE entries "
"SET summary = '', read = 1 "
"WHERE id = ?"
)
cur.execute(sql, (ix,))
async def mark_source_as_read(db_file, source):
"""
Set read status of entries of given source as read.
Parameters
----------
db_file : str
Path to database file.
source : str
URL.
"""
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"UPDATE entries "
"SET summary = '', read = 1 "
"WHERE source = ?"
)
cur.execute(sql, (source,))
async def mark_all_as_read(db_file):
"""
Set read status of all entries as read.
Parameters
----------
db_file : str
Path to database file.
"""
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"UPDATE entries "
"SET summary = '', read = 1 "
)
cur.execute(sql)
sql = (
"DELETE FROM archive"
)
cur.execute(sql)
async def delete_entry(cur, ix):
"""
Delete entry from table archive.
Parameters
----------
db_file : str
Path to database file.
ix : str
Index of entry.
"""
sql = (
"DELETE FROM archive "
"WHERE id = ?"
)
cur.execute(sql, (ix,))
async def statistics(db_file):
"""
Return table statistics.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
msg : str
Statistics as message.
"""
feeds = await get_number_of_items(db_file, 'feeds')
active_feeds = await get_number_of_feeds_active(db_file)
entries = await get_number_of_items(db_file, 'entries')
unread_entries = await get_number_of_entries_unread(db_file)
# msg = """You have {} unread news items out of {} from {} news sources.
# """.format(unread_entries, entries, feeds)
with create_connection(db_file) as conn:
cur = conn.cursor()
keys = []
for key in ["enabled", "interval", "quantum"]:
sql = (
"SELECT value "
"FROM settings "
"WHERE key = ?"
)
keys.extend([cur.execute(sql, (key,)).fetchone()[0]])
msg = (
"```\n"
"News items : {} ({})\n"
"News sources : {} ({})\n"
"Update interval : {}\n"
"Items per update : {}\n"
"Operation status : {}\n"
"```"
).format(
unread_entries, entries,
active_feeds, feeds,
keys[1],
keys[2],
keys[0]
)
return msg
async def update_statistics(cur):
"""
Update table statistics.
Parameters
----------
cur : object
Cursor object.
"""
stat_dict = {}
stat_dict["feeds"] = await get_number_of_items(cur, 'feeds')
stat_dict["entries"] = await get_number_of_items(cur, 'entries')
stat_dict["unread"] = await get_number_of_entries_unread(cur=cur)
for i in stat_dict:
sql = (
"SELECT id "
"FROM statistics "
"WHERE title = ?"
)
cur.execute(sql, (i,))
if cur.fetchone():
sql = (
"UPDATE statistics "
"SET number = :num "
"WHERE title = :title"
)
cur.execute(sql, {
"title": i,
"num": stat_dict[i]
})
else:
sql = (
"SELECT count(id) "
"FROM statistics"
)
count = cur.execute(sql).fetchone()[0]
ix = count + 1
sql = (
"INSERT INTO statistics "
"VALUES(?,?,?)"
)
cur.execute(sql, (ix, i, stat_dict[i]))
async def toggle_status(db_file, ix):
"""
Toggle status of feed.
Parameters
----------
db_file : str
Path to database file.
ix : str
Index of entry.
Returns
-------
msg : str
Message.
"""
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
try:
#cur = get_cursor(db_file)
sql = (
"SELECT name "
"FROM feeds "
"WHERE id = :id"
)
title = cur.execute(sql, (ix,)).fetchone()[0]
sql = (
"SELECT enabled "
"FROM feeds "
"WHERE id = ?"
)
# NOTE [0][1][2]
status = cur.execute(sql, (ix,)).fetchone()[0]
# FIXME always set to 1
# NOTE Maybe because is not integer
# TODO Reset feed table before further testing
if status == 1:
status = 0
state = "disabled"
else:
status = 1
state = "enabled"
sql = (
"UPDATE feeds "
"SET enabled = :status "
"WHERE id = :id"
)
cur.execute(sql, {
"status": status,
"id": ix
})
msg = (
"Updates from '{}' are now {}."
).format(title, state)
except:
msg = (
"No news source with ID {}."
).format(ix)
return msg
async def set_date(cur, url):
"""
Set last update date of feed.
Parameters
----------
cur : object
Cursor object.
url : str
URL.
"""
today = date.today()
sql = (
"UPDATE feeds "
"SET updated = :today "
"WHERE address = :url"
)
# cur = conn.cursor()
cur.execute(sql, {
"today": today,
"url": url
})
async def add_entry_and_set_date(db_file, source, entry):
"""
Add entry to table entries and set date of source in table feeds.
Parameters
----------
db_file : str
Path to database file.
source : str
Feed URL.
entry : list
Entry properties.
"""
# TODO While `async with DBLOCK` does work well from
# outside of functions, it would be better practice
# to place it within the functions.
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
await add_entry(cur, entry)
await set_date(cur, source)
async def update_source_status(db_file, status, source):
"""
Set HTTP status of source in table feeds.
Parameters
----------
db_file : str
Path to database file.
source : str
Feed URL.
status : str
Status ID or message.
"""
sql = (
"UPDATE feeds "
"SET status = :status, scanned = :scanned "
"WHERE address = :url"
)
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
cur.execute(sql, {
"status" : status,
"scanned" : date.today(),
"url" : source
})
async def update_source_validity(db_file, source, valid):
"""
Set validity status of source in table feeds.
Parameters
----------
db_file : str
Path to database file.
source : str
Feed URL.
valid : boolean
0 or 1.
"""
sql = (
"UPDATE feeds "
"SET valid = :validity "
"WHERE address = :url"
)
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
cur.execute(sql, {
"validity": valid,
"url": source
})
"""
TODO
Investigate why causes entry[6] (date) to be int 0
"""
async def add_entry(cur, entry):
"""
Add a new entry row into the entries table.
Parameters
----------
cur : object
Cursor object.
entry : str
Entry properties.
"""
sql = (
"INSERT "
"INTO entries("
"title, "
"summary, "
"link, "
"entry_id, "
"source, "
"timestamp, "
"read"
") "
"VALUES(?, ?, ?, ?, ?, ?, ?)"
)
try:
cur.execute(sql, entry)
except:
print(entry[6])
print(type(entry[6]))
print(entry)
print(type(entry))
breakpoint()
# NOTE See remove_nonexistent_entries
# NOTE This function doesn't work as expected with bbs and docuwiki feeds
async def remove_entry(db_file, source, length):
"""
Maintain list of entries equal to feed.
Check the number returned by feed and delete
existing entries up to the same returned amount.
Parameters
----------
db_file : str
Path to database file.
source : str
Feed URL.
length : str
Number.
"""
# FIXED
# Dino empty titles are not counted https://dino.im/index.xml
# SOLVED
# Add text if is empty
# title = '*** No title ***' if not entry.title else entry.title
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT count(id) "
"FROM entries "
"WHERE source = ?"
)
count = cur.execute(sql, (source,)).fetchone()[0]
limit = count - length
if limit:
limit = limit;
sql = (
"DELETE FROM entries "
"WHERE id "
"IN (SELECT id "
"FROM entries "
"WHERE source = :source "
"ORDER BY id "
"ASC LIMIT :limit)"
)
cur.execute(sql, {
"source": source,
"limit": limit
})
# TODO Move entries that don't exist into table archive.
# NOTE Entries that are read from archive are deleted.
# NOTE Unlike entries from table entries, entries from
# table archive are not marked as read.
async def remove_nonexistent_entries(db_file, feed, source):
"""
Remove entries that don't exist in a given parsed feed.
Check the entries returned from feed and delete read non
existing entries, otherwise move to table archive, if unread.
Parameters
----------
db_file : str
Path to database file.
feed : list
Parsed feed document.
source : str
Feed URL. URL of associated feed.
"""
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT id, title, link, entry_id, timestamp, read "
"FROM entries "
"WHERE source = ?"
)
items = cur.execute(sql, (source,)).fetchall()
entries = feed.entries
# breakpoint()
for item in items:
valid = False
for entry in entries:
title = None
link = None
time = None
# valid = False
# TODO better check and don't repeat code
if entry.has_key("id") and item[3]:
if entry.id == item[3]:
# print("compare1:", entry.id)
# print("compare2:", item[3])
# print("============")
valid = True
break
else:
if entry.has_key("title"):
title = entry.title
else:
title = feed["feed"]["title"]
if entry.has_key("link"):
link = await datahandler.join_url(source, entry.link)
else:
link = source
if entry.has_key("published") and item[4]:
# print("compare11:", title, link, time)
# print("compare22:", item[1], item[2], item[4])
# print("============")
time = await datetimehandler.rfc2822_to_iso8601(entry.published)
if (item[1] == title and
item[2] == link and
item[4] == time):
valid = True
break
else:
if (item[1] == title and
item[2] == link):
# print("compare111:", title, link)
# print("compare222:", item[1], item[2])
# print("============")
valid = True
break
# TODO better check and don't repeat code
if not valid:
# print("id: ", item[0])
# if title:
# print("title: ", title)
# print("item[1]: ", item[1])
# if link:
# print("link: ", link)
# print("item[2]: ", item[2])
# if entry.id:
# print("last_entry:", entry.id)
# print("item[3]: ", item[3])
# if time:
# print("time: ", time)
# print("item[4]: ", item[4])
# print("read: ", item[5])
# breakpoint()
async with DBLOCK:
# TODO Send to table archive
# TODO Also make a regular/routine check for sources that
# have been changed (though that can only happen when
# manually editing)
ix = item[0]
# print(">>> SOURCE: ", source)
# print(">>> INVALID:", item[1])
# print("title:", item[1])
# print("link :", item[2])
# print("id :", item[3])
if item[5] == 1:
# print(">>> DELETING:", item[1])
sql = (
"DELETE "
"FROM entries "
"WHERE id = :ix"
)
cur.execute(sql, (ix,))
else:
# print(">>> ARCHIVING:", item[1])
sql = (
"INSERT "
"INTO archive "
"SELECT * "
"FROM entries "
"WHERE entries.id = :ix"
)
try:
cur.execute(sql, (ix,))
except:
print(
"ERROR DB inset from entries "
"into archive at index", ix
)
sql = (
"DELETE "
"FROM entries "
"WHERE id = :ix"
)
try:
cur.execute(sql, (ix,))
except:
print(
"ERROR DB deleting entries "
"from entries at index", ix
)
async def get_feeds(db_file):
"""
Query table feeds for Title, URL, Categories, Tags.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
result : list
Title, URL, Categories, Tags of feeds.
"""
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT name, address, type, categories, tags "
"FROM feeds"
)
result = cur.execute(sql).fetchall()
return result
async def get_feeds_url(db_file):
"""
Query active feeds for URLs.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
result : list
URLs of active feeds.
"""
with create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"SELECT address "
"FROM feeds "
"WHERE enabled = 1"
)
result = cur.execute(sql).fetchall()
return result
async def list_feeds(db_file):
"""
Query table feeds and list items.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
msg : str
URLs of feeds as message.
"""
cur = get_cursor(db_file)
sql = (
"SELECT name, address, updated, enabled, id "
"FROM feeds"
)
results = cur.execute(sql)
feeds_list = "\nList of subscriptions:\n```\n"
counter = 0
for result in results:
counter += 1
feeds_list += (
"Name : {}\n"
"Address : {}\n"
"Updated : {}\n"
"Status : {}\n"
"ID : {}\n"
"\n"
).format(
str(result[0]),
str(result[1]),
str(result[2]),
str(result[3]),
str(result[4])
)
if counter:
return feeds_list + (
"```\nTotal of {} subscriptions.\n"
).format(counter)
else:
msg = (
"List of subscriptions is empty.\n"
"To add feed, send a URL\n"
"Try these:\n"
# TODO Pick random from featured/recommended
"https://reclaimthenet.org/feed/"
)
return msg
async def last_entries(db_file, num):
"""
Query entries
Parameters
----------
db_file : str
Path to database file.
num : str
Number.
Returns
-------
titles_list : str
List of recent N entries as message.
"""
num = int(num)
if num > 50:
num = 50
elif num < 1:
num = 1
cur = get_cursor(db_file)
# sql = (
# "SELECT title, link "
# "FROM entries "
# "ORDER BY ROWID DESC "
# "LIMIT :num"
# )
sql = (
"SELECT title, link "
"FROM entries "
"WHERE read = 0 "
"ORDER BY timestamp DESC "
"LIMIT :num "
)
results = cur.execute(sql, (num,))
titles_list = "Recent {} titles:\n```".format(num)
counter = 0
for result in results:
counter += 1
titles_list += (
"\n{}\n{}\n"
).format(
str(result[0]),
str(result[1])
)
if counter:
titles_list += "```\n"
return titles_list
else:
return "There are no news at the moment."
async def search_feeds(db_file, query):
"""
Query feeds.
Parameters
----------
db_file : str
Path to database file.
query : str
Search query.
Returns
-------
titles_list : str
Feeds of specified keywords as message.
"""
cur = get_cursor(db_file)
sql = (
"SELECT name, address, id, enabled "
"FROM feeds "
"WHERE name LIKE ? "
"OR address LIKE ? "
"LIMIT 50"
)
results = cur.execute(sql, [f'%{query}%', f'%{query}%'])
results_list = (
"Feeds containing '{}':\n```"
).format(query)
counter = 0
for result in results:
counter += 1
results_list += (
"\nName : {}"
"\nURL : {}"
"\nIndex : {}"
"\nMode : {}"
"\n"
).format(
str(result[0]),
str(result[1]),
str(result[2]),
str(result[3])
)
if counter:
return results_list + "\n```\nTotal of {} feeds".format(counter)
else:
return "No feeds were found for: {}".format(query)
async def search_entries(db_file, query):
"""
Query entries.
Parameters
----------
db_file : str
Path to database file.
query : str
Search query.
Returns
-------
titles_list : str
Entries of specified keywords as message.
"""
cur = get_cursor(db_file)
sql = (
"SELECT title, link "
"FROM entries "
"WHERE title LIKE ? "
"UNION ALL "
"SELECT title, link "
"FROM archive "
"WHERE title LIKE ? "
"LIMIT 50"
)
results = cur.execute(sql, (
f'%{query}%',
f'%{query}%'
))
results_list = (
"Search results for '{}':\n```"
).format(query)
counter = 0
for result in results:
counter += 1
results_list += (
"\n{}\n{}\n"
).format(
str(result[0]),
str(result[1])
)
if counter:
return results_list + "```\nTotal of {} results".format(counter)
else:
return "No results were found for: {}".format(query)
"""
FIXME
Error due to missing date, but it appears that date is present:
ERROR DATE: source = https://blog.heckel.io/feed/
ERROR DATE: date = 2008-05-13T13:51:50+00:00
ERROR DATE: result = https://blog.heckel.io/feed/
19:32:05 ERROR DATE: source = https://mwl.io/feed
19:32:05 ERROR DATE: date = 2023-11-30T10:56:39+00:00
19:32:05 ERROR DATE: result = https://mwl.io/feed
19:32:05 ERROR DATE: source = https://mwl.io/feed
19:32:05 ERROR DATE: date = 2023-11-22T16:59:08+00:00
19:32:05 ERROR DATE: result = https://mwl.io/feed
19:32:06 ERROR DATE: source = https://mwl.io/feed
19:32:06 ERROR DATE: date = 2023-11-16T10:33:57+00:00
19:32:06 ERROR DATE: result = https://mwl.io/feed
19:32:06 ERROR DATE: source = https://mwl.io/feed
19:32:06 ERROR DATE: date = 2023-11-09T07:37:57+00:00
19:32:06 ERROR DATE: result = https://mwl.io/feed
"""
async def check_entry_exist(db_file, source, eid=None,
title=None, link=None, date=None):
"""
Check whether an entry exists.
If entry has an ID, check by ID.
If entry has timestamp, check by title, link and date.
Otherwise, check by title and link.
Parameters
----------
db_file : str
Path to database file.
source : str
Feed URL. URL of associated feed.
eid : str, optional
Entry ID. The default is None.
title : str, optional
Entry title. The default is None.
link : str, optional
Entry URL. The default is None.
date : str, optional
Entry Timestamp. The default is None.
Returns
-------
bool
True or None.
"""
cur = get_cursor(db_file)
if eid:
sql = (
"SELECT id "
"FROM entries "
"WHERE entry_id = :eid and source = :source"
)
result = cur.execute(sql, {
"eid": eid,
"source": source
}).fetchone()
elif date:
sql = (
"SELECT id "
"FROM entries "
"WHERE "
"title = :title and "
"link = :link and "
"timestamp = :date"
)
try:
result = cur.execute(sql, {
"title": title,
"link": link,
"timestamp": date
}).fetchone()
except:
print(await datetimehandler.current_time(), "ERROR DATE: source =", source)
print(await datetimehandler.current_time(), "ERROR DATE: date =", date)
else:
sql = (
"SELECT id "
"FROM entries "
"WHERE title = :title and link = :link"
)
result = cur.execute(sql, {
"title": title,
"link": link
}).fetchone()
try:
if result:
return True
else:
None
except:
print(await datetimehandler.current_time(), "ERROR DATE: result =", source)
async def set_settings_value(db_file, key_value):
"""
Set settings value.
Parameters
----------
db_file : str
Path to database file.
key_value : list
key : str
enabled, filter-allow, filter-deny,
interval, masters, quantum, random.
value : int
Numeric value.
"""
# if isinstance(key_value, list):
# key = key_value[0]
# val = key_value[1]
# elif key_value == "enable":
# key = "enabled"
# val = 1
# else:
# key = "enabled"
# val = 0
key = key_value[0]
val = key_value[1]
async with DBLOCK:
with create_connection(db_file) as conn:
cur = conn.cursor()
await set_settings_value_default(cur, key)
sql = (
"UPDATE settings "
"SET value = :value "
"WHERE key = :key"
)
cur.execute(sql, {
"key": key,
"value": val
})
# TODO Place settings also in a file
async def set_settings_value_default(cur, key):
"""
Set default settings value, if no value found.
Parameters
----------
cur : object
Cursor object.
key : str
Key: enabled, interval, master, quantum, random.
Returns
-------
val : str
Numeric value.
"""
# async def set_settings_value_default(cur):
# keys = ["enabled", "interval", "quantum"]
# for i in keys:
# sql = "SELECT id FROM settings WHERE key = ?"
# cur.execute(sql, (i,))
# if not cur.fetchone():
# val = await settings.get_value_default(i)
# sql = "INSERT INTO settings(key,value) VALUES(?,?)"
# cur.execute(sql, (i, val))
sql = (
"SELECT id "
"FROM settings "
"WHERE key = ?"
)
cur.execute(sql, (key,))
if not cur.fetchone():
val = await confighandler.get_value_default(key)
sql = (
"INSERT "
"INTO settings(key,value) "
"VALUES(?,?)"
)
cur.execute(sql, (key, val))
return val
async def get_settings_value(db_file, key):
"""
Get settings value.
Parameters
----------
db_file : str
Path to database file.
key : str
Key: "enabled", "interval", "master", "quantum", "random".
Returns
-------
val : str
Numeric value.
"""
# try:
# with create_connection(db_file) as conn:
# cur = conn.cursor()
# sql = "SELECT value FROM settings WHERE key = ?"
# cur.execute(sql, (key,))
# result = cur.fetchone()
# except:
# result = await settings.get_value_default(key)
# if not result:
# result = await settings.get_value_default(key)
# return result
with create_connection(db_file) as conn:
try:
cur = conn.cursor()
sql = (
"SELECT value "
"FROM settings "
"WHERE key = ?"
)
val = cur.execute(sql, (key,)).fetchone()[0]
except:
val = await set_settings_value_default(cur, key)
if not val:
val = await set_settings_value_default(cur, key)
return val