grundle
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
This commit is contained in:
416
native-route/linux/src/database/feed-item-store.vala
Normal file
416
native-route/linux/src/database/feed-item-store.vala
Normal file
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
* FeedItemStore.vala
|
||||
*
|
||||
* CRUD operations for feed items with FTS search support.
|
||||
*/
|
||||
|
||||
/**
|
||||
* FeedItemStore - Manages feed item persistence
|
||||
*/
|
||||
public class RSSuper.FeedItemStore : Object {
|
||||
private Database db;
|
||||
|
||||
/**
|
||||
* Signal emitted when an item is added
|
||||
*/
|
||||
public signal void item_added(FeedItem item);
|
||||
|
||||
/**
|
||||
* Signal emitted when an item is updated
|
||||
*/
|
||||
public signal void item_updated(FeedItem item);
|
||||
|
||||
/**
|
||||
* Signal emitted when an item is deleted
|
||||
*/
|
||||
public signal void item_deleted(string id);
|
||||
|
||||
/**
|
||||
* Create a new feed item store
|
||||
*/
|
||||
public FeedItemStore(Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new feed item
|
||||
*/
|
||||
public FeedItem add(FeedItem item) throws Error {
|
||||
var stmt = db.prepare(
|
||||
"INSERT INTO feed_items (id, subscription_id, title, link, description, content, " +
|
||||
"author, published, updated, categories, enclosure_url, enclosure_type, " +
|
||||
"enclosure_length, guid, is_read, is_starred) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
|
||||
);
|
||||
|
||||
stmt.bind_text(1, item.id, -1, null);
|
||||
stmt.bind_text(2, item.subscription_title ?? "", -1, null);
|
||||
stmt.bind_text(3, item.title, -1, null);
|
||||
stmt.bind_text(4, item.link ?? "", -1, null);
|
||||
stmt.bind_text(5, item.description ?? "", -1, null);
|
||||
stmt.bind_text(6, item.content ?? "", -1, null);
|
||||
stmt.bind_text(7, item.author ?? "", -1, null);
|
||||
stmt.bind_text(8, item.published ?? "", -1, null);
|
||||
stmt.bind_text(9, item.updated ?? "", -1, null);
|
||||
stmt.bind_text(10, format_categories(item.categories), -1, null);
|
||||
stmt.bind_text(11, item.enclosure_url ?? "", -1, null);
|
||||
stmt.bind_text(12, item.enclosure_type ?? "", -1, null);
|
||||
stmt.bind_text(13, item.enclosure_length ?? "", -1, null);
|
||||
stmt.bind_text(14, item.guid ?? "", -1, null);
|
||||
stmt.bind_int(15, 0); // is_read
|
||||
stmt.bind_int(16, 0); // is_starred
|
||||
|
||||
stmt.step();
|
||||
|
||||
debug("Feed item added: %s", item.id);
|
||||
item_added(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple items in a batch
|
||||
*/
|
||||
public void add_batch(FeedItem[] items) throws Error {
|
||||
db.begin_transaction();
|
||||
try {
|
||||
foreach (var item in items) {
|
||||
add(item);
|
||||
}
|
||||
db.commit();
|
||||
debug("Batch insert completed: %d items", items.length);
|
||||
} catch (Error e) {
|
||||
db.rollback();
|
||||
throw new DBError.FAILED("Transaction failed: %s".printf(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item by ID
|
||||
*/
|
||||
public FeedItem? get_by_id(string id) throws Error {
|
||||
var stmt = db.prepare(
|
||||
"SELECT id, subscription_id, title, link, description, content, author, " +
|
||||
"published, updated, categories, enclosure_url, enclosure_type, " +
|
||||
"enclosure_length, guid, is_read, is_starred " +
|
||||
"FROM feed_items WHERE id = ?;"
|
||||
);
|
||||
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
|
||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
return row_to_item(stmt);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items by subscription ID
|
||||
*/
|
||||
public FeedItem[] get_by_subscription(string subscription_id) throws Error {
|
||||
var items = new GLib.List<FeedItem?>();
|
||||
|
||||
var stmt = db.prepare(
|
||||
"SELECT id, subscription_id, title, link, description, content, author, " +
|
||||
"published, updated, categories, enclosure_url, enclosure_type, " +
|
||||
"enclosure_length, guid, is_read, is_starred " +
|
||||
"FROM feed_items WHERE subscription_id = ? " +
|
||||
"ORDER BY published DESC LIMIT 100;"
|
||||
);
|
||||
|
||||
stmt.bind_text(1, subscription_id, -1, null);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items_to_array(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items
|
||||
*/
|
||||
public FeedItem[] get_all() throws Error {
|
||||
var items = new GLib.List<FeedItem?>();
|
||||
|
||||
var stmt = db.prepare(
|
||||
"SELECT id, subscription_id, title, link, description, content, author, " +
|
||||
"published, updated, categories, enclosure_url, enclosure_type, " +
|
||||
"enclosure_length, guid, is_read, is_starred " +
|
||||
"FROM feed_items ORDER BY published DESC LIMIT 1000;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items_to_array(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search items using FTS
|
||||
*/
|
||||
public FeedItem[] search(string query, int limit = 50) throws Error {
|
||||
var items = new GLib.List<FeedItem?>();
|
||||
|
||||
var stmt = db.prepare(
|
||||
"SELECT f.id, f.subscription_id, f.title, f.link, f.description, f.content, " +
|
||||
"f.author, f.published, f.updated, f.categories, f.enclosure_url, " +
|
||||
"f.enclosure_type, f.enclosure_length, f.guid, f.is_read, f.is_starred " +
|
||||
"FROM feed_items_fts t " +
|
||||
"JOIN feed_items f ON t.rowid = f.rowid " +
|
||||
"WHERE feed_items_fts MATCH ? " +
|
||||
"ORDER BY rank " +
|
||||
"LIMIT ?;"
|
||||
);
|
||||
|
||||
stmt.bind_text(1, query, -1, null);
|
||||
stmt.bind_int(2, limit);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items_to_array(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an item as read
|
||||
*/
|
||||
public void mark_as_read(string id) throws Error {
|
||||
var stmt = db.prepare("UPDATE feed_items SET is_read = 1 WHERE id = ?;");
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
stmt.step();
|
||||
|
||||
debug("Item marked as read: %s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an item as unread
|
||||
*/
|
||||
public void mark_as_unread(string id) throws Error {
|
||||
var stmt = db.prepare("UPDATE feed_items SET is_read = 0 WHERE id = ?;");
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
stmt.step();
|
||||
|
||||
debug("Item marked as unread: %s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an item as starred
|
||||
*/
|
||||
public void mark_as_starred(string id) throws Error {
|
||||
var stmt = db.prepare("UPDATE feed_items SET is_starred = 1 WHERE id = ?;");
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
stmt.step();
|
||||
|
||||
debug("Item starred: %s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark an item from starred
|
||||
*/
|
||||
public void unmark_starred(string id) throws Error {
|
||||
var stmt = db.prepare("UPDATE feed_items SET is_starred = 0 WHERE id = ?;");
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
stmt.step();
|
||||
|
||||
debug("Item unstarred: %s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread items
|
||||
*/
|
||||
public FeedItem[] get_unread() throws Error {
|
||||
var items = new GLib.List<FeedItem?>();
|
||||
|
||||
var stmt = db.prepare(
|
||||
"SELECT id, subscription_id, title, link, description, content, author, " +
|
||||
"published, updated, categories, enclosure_url, enclosure_type, " +
|
||||
"enclosure_length, guid, is_read, is_starred " +
|
||||
"FROM feed_items WHERE is_read = 0 " +
|
||||
"ORDER BY published DESC LIMIT 100;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items_to_array(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starred items
|
||||
*/
|
||||
public FeedItem[] get_starred() throws Error {
|
||||
var items = new GLib.List<FeedItem?>();
|
||||
|
||||
var stmt = db.prepare(
|
||||
"SELECT id, subscription_id, title, link, description, content, author, " +
|
||||
"published, updated, categories, enclosure_url, enclosure_type, " +
|
||||
"enclosure_length, guid, is_read, is_starred " +
|
||||
"FROM feed_items WHERE is_starred = 1 " +
|
||||
"ORDER BY published DESC LIMIT 100;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items_to_array(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an item by ID
|
||||
*/
|
||||
public void delete(string id) throws Error {
|
||||
var stmt = db.prepare("DELETE FROM feed_items WHERE id = ?;");
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
stmt.step();
|
||||
|
||||
debug("Item deleted: %s", id);
|
||||
item_deleted(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete items by subscription ID
|
||||
*/
|
||||
public void delete_by_subscription(string subscription_id) throws Error {
|
||||
var stmt = db.prepare("DELETE FROM feed_items WHERE subscription_id = ?;");
|
||||
stmt.bind_text(1, subscription_id, -1, null);
|
||||
stmt.step();
|
||||
|
||||
debug("Items deleted for subscription: %s", subscription_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old items (keep last N items per subscription)
|
||||
*/
|
||||
public void cleanup_old_items(int keep_count = 100) throws Error {
|
||||
db.begin_transaction();
|
||||
try {
|
||||
var stmt = db.prepare(
|
||||
"DELETE FROM feed_items WHERE id NOT IN (" +
|
||||
"SELECT id FROM feed_items " +
|
||||
"ORDER BY published DESC " +
|
||||
"LIMIT -1 OFFSET ?" +
|
||||
");"
|
||||
);
|
||||
stmt.bind_int(1, keep_count);
|
||||
stmt.step();
|
||||
|
||||
db.commit();
|
||||
debug("Old items cleaned up, kept %d", keep_count);
|
||||
} catch (Error e) {
|
||||
db.rollback();
|
||||
throw new DBError.FAILED("Transaction failed: %s".printf(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a database row to a FeedItem
|
||||
*/
|
||||
private FeedItem? row_to_item(SQLite.Stmt stmt) {
|
||||
try {
|
||||
string categories_str = stmt.column_text(9);
|
||||
string[] categories = parse_categories(categories_str);
|
||||
|
||||
var item = new FeedItem.with_values(
|
||||
stmt.column_text(0), // id
|
||||
stmt.column_text(2), // title
|
||||
stmt.column_text(3), // link
|
||||
stmt.column_text(4), // description
|
||||
stmt.column_text(5), // content
|
||||
stmt.column_text(6), // author
|
||||
stmt.column_text(7), // published
|
||||
stmt.column_text(8), // updated
|
||||
categories,
|
||||
stmt.column_text(10), // enclosure_url
|
||||
stmt.column_text(11), // enclosure_type
|
||||
stmt.column_text(12), // enclosure_length
|
||||
stmt.column_text(13), // guid
|
||||
stmt.column_text(1) // subscription_id (stored as subscription_title)
|
||||
);
|
||||
|
||||
return item;
|
||||
} catch (Error e) {
|
||||
warning("Failed to parse item row: %s", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format categories array as JSON string
|
||||
*/
|
||||
private string format_categories(string[] categories) {
|
||||
if (categories.length == 0) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
for (var i = 0; i < categories.length; i++) {
|
||||
if (i > 0) sb.append(",");
|
||||
sb.append("\"");
|
||||
sb.append(categories[i]);
|
||||
sb.append("\"");
|
||||
}
|
||||
sb.append("]");
|
||||
|
||||
return sb.str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse categories from JSON string
|
||||
*/
|
||||
private string[] parse_categories(string json) {
|
||||
if (json == null || json.length == 0 || json == "[]") {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
var parser = new Json.Parser();
|
||||
if (parser.load_from_data(json)) {
|
||||
var node = parser.get_root();
|
||||
if (node.get_node_type() == Json.NodeType.ARRAY) {
|
||||
var array = node.get_array();
|
||||
var categories = new string[array.get_length()];
|
||||
for (var i = 0; i < array.get_length(); i++) {
|
||||
categories[i] = array.get_string_element(i);
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
} catch (Error e) {
|
||||
warning("Failed to parse categories: %s", e.message);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private FeedItem[] items_to_array(GLib.List<FeedItem?> list) {
|
||||
FeedItem[] arr = {};
|
||||
for (unowned var node = list; node != null; node = node.next) {
|
||||
if (node.data != null) arr += node.data;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user