SharpRSS/SharpRss/Services/DatabaseService.cs

325 lines
16 KiB
C#
Raw Normal View History

2023-05-18 01:27:11 +02:00
using System;
using System.Collections.Generic;
using System.Data;
2023-05-18 01:27:11 +02:00
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Data.Sqlite;
using Serilog;
using SharpRss.Models;
namespace SharpRss.Services
{
internal class DatabaseService : IDisposable
2023-05-18 01:27:11 +02:00
{
internal DatabaseService()
{
_sqlConn = new SqliteConnection(_connectionString);
InitializeDb();
2023-05-18 01:27:11 +02:00
}
private readonly SqliteConnection _sqlConn;
2023-05-20 00:04:45 +02:00
private readonly string _connectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.sqlite")};";
private readonly string _groupTable = "group_data";
private readonly string _feedTable = "feed_data";
private readonly string _feedItemTable = "feed_item_data";
2023-05-18 01:27:11 +02:00
2023-05-22 15:55:21 +02:00
// Groups
public async Task<HashSet<GroupModel>> GetGroupsAsync(string? groupId = null)
2023-05-20 00:04:45 +02:00
{
2023-05-21 21:56:37 +02:00
_sqlConn.Open();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand(groupId != null ? $"SELECT * FROM {_groupTable} WHERE id=@gId;" : $"SELECT * FROM {_groupTable}", _sqlConn)
2023-05-21 21:56:37 +02:00
{
Parameters =
{
new SqliteParameter("gId", groupId)
2023-05-21 21:56:37 +02:00
}
};
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
2023-05-22 15:55:21 +02:00
HashSet<GroupModel> groups = new HashSet<GroupModel>();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmdFeedCount = new SqliteCommand($"SELECT COUNT(*) FROM {_feedTable} WHERE group_id=@groupId", _sqlConn);
2023-05-21 21:56:37 +02:00
while (reader.Read())
{
2023-05-23 15:04:02 +02:00
cmdFeedCount.Parameters.Clear();
cmdFeedCount.Parameters.Add(new SqliteParameter("groupId", reader["id"].ToString()));
using SqliteDataReader countReader = await cmdFeedCount.ExecuteReaderAsync();
int count = countReader.Read() ? countReader.GetInt32(0) : 0;
groups.Add(new GroupModel()
2023-05-21 21:56:37 +02:00
{
Name = reader["name"].ToString(),
2023-05-23 15:04:02 +02:00
FeedCount = count,
2023-05-21 21:56:37 +02:00
HexColor = reader["hex_color"].ToString(),
Icon = reader["icon"].ToString(),
Id = reader["id"].ToString()
});
}
_sqlConn.Close();
return groups;
2023-05-20 00:04:45 +02:00
}
2023-05-21 21:56:37 +02:00
/// <summary>
2023-05-22 15:55:21 +02:00
/// Creates a group if not exists then update the group.
2023-05-21 21:56:37 +02:00
/// </summary>
/// <param name="groupModel"></param>
/// <returns></returns>
public async Task<bool> SetGroupAsync(GroupModel groupModel)
2023-05-18 01:27:11 +02:00
{
2023-05-21 21:56:37 +02:00
bool result = false;
2023-05-18 01:27:11 +02:00
_sqlConn.Open();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_groupTable} (id, hex_color, icon, name) VALUES (IFNULL((SELECT id FROM {_groupTable} WHERE name=@name), @id), @hexColor, @icon, @name)", _sqlConn)
2023-05-18 01:27:11 +02:00
{
2023-05-21 21:56:37 +02:00
Parameters =
{
new SqliteParameter("id", groupModel.Id),
new SqliteParameter("hexColor", groupModel.HexColor),
new SqliteParameter("icon", groupModel.Icon),
new SqliteParameter("name", groupModel.Name)
}
};
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
2023-05-18 01:27:11 +02:00
_sqlConn.Close();
return result;
}
2023-05-21 21:56:37 +02:00
public async Task<bool> RemoveGroupAsync(GroupModel groupModel)
2023-05-18 01:27:11 +02:00
{
2023-05-21 21:56:37 +02:00
bool result = false;
2023-05-18 01:27:11 +02:00
_sqlConn.Open();
2023-05-22 13:26:27 +02:00
// Remove the group and remove the feeds that were part of the group.
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_groupTable} WHERE id=@id; UPDATE {_feedTable} SET group_id=NULL WHERE group_id=@id", _sqlConn)
2023-05-18 01:27:11 +02:00
{
2023-05-21 21:56:37 +02:00
Parameters =
{
new SqliteParameter("id", groupModel.Id)
}
};
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
2023-05-18 01:27:11 +02:00
_sqlConn.Close();
return result;
}
2023-05-22 15:55:21 +02:00
// Feeds
2023-05-23 15:04:02 +02:00
public async Task<HashSet<FeedModel>> GetFeedsAsync(string? groupId = null)
2023-05-18 01:27:11 +02:00
{
2023-05-21 21:56:37 +02:00
HashSet<FeedModel> feeds = new HashSet<FeedModel>();
2023-05-18 01:27:11 +02:00
_sqlConn.Open();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand(groupId != null ? $"SELECT * FROM {_feedTable} WHERE group_id=@groupId" : $"SELECT * FROM {_feedTable}", _sqlConn)
2023-05-20 00:04:45 +02:00
{
2023-05-21 21:56:37 +02:00
Parameters =
{
2023-05-23 15:04:02 +02:00
new SqliteParameter("groupId", groupId == null ? string.Empty : groupId)
2023-05-21 21:56:37 +02:00
}
};
2023-05-22 15:55:21 +02:00
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
while (reader.Read())
{
feeds.Add(ReaderToFeedModel(reader));
2023-05-22 15:55:21 +02:00
}
2023-05-18 01:27:11 +02:00
_sqlConn.Close();
2023-05-21 21:56:37 +02:00
return feeds;
2023-05-18 01:27:11 +02:00
}
private FeedModel ReaderToFeedModel(SqliteDataReader reader)
2023-05-18 01:27:11 +02:00
{
return new FeedModel(reader["url"].ToString())
{
Id = reader["id"].ToString(),
Title = reader["title"].ToString(),
GroupId = reader["group_id"].ToString(),
FeedType = reader["feed_type"].ToString(),
Description = reader["description"].ToString(),
Language = reader["language"].ToString(),
Copyright = reader["copyright"].ToString(),
DateAdded = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["date_added"].ToString(), out long parsedVal) ? parsedVal : 0),
LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["last_updated"].ToString(), out long lastUpdated) ? lastUpdated : 0),
ImageUrl = reader["image_url"].ToString(),
OriginalDocument = reader["original_document"].ToString()
};
}
public async Task<FeedModel?> SetFeedAsync(FeedModel feedModel)
{
FeedModel? resultModel = null;
2023-05-18 01:27:11 +02:00
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedTable} (id, url, title, group_id, feed_type, description, language, copyright, date_added, last_updated, image_url, original_document) VALUES (IFNULL((SELECT id FROM {_feedTable} WHERE url=@url), @id), @url, @title, @groupId, @feedType, @description, @language, @copyright, IFNULL((SELECT date_added FROM {_feedTable} WHERE id=@id), @dateAdded), @lastUpdated, @imageUrl, @originalDoc); SELECT * FROM {_feedTable} WHERE url=@url", _sqlConn)
2023-05-20 00:04:45 +02:00
{
2023-05-21 21:56:37 +02:00
Parameters =
{
new SqliteParameter("id", feedModel.Id ?? string.Empty),
new SqliteParameter("url", feedModel.Url ?? string.Empty),
new SqliteParameter("title", feedModel.Title ?? string.Empty),
new SqliteParameter("groupId", feedModel.GroupId ?? string.Empty),
new SqliteParameter("feedType", feedModel.FeedType ?? string.Empty),
new SqliteParameter("description", feedModel.Description ?? string.Empty),
new SqliteParameter("language", feedModel.Language ?? string.Empty),
new SqliteParameter("copyright", feedModel.Copyright ?? string.Empty),
2023-05-24 19:27:22 +02:00
new SqliteParameter("dateAdded", feedModel.DateAdded?.ToUnixTimeMilliseconds()),
new SqliteParameter("lastUpdated", feedModel.LastUpdated?.ToUnixTimeMilliseconds()),
new SqliteParameter("imageUrl", feedModel.ImageUrl ?? string.Empty),
new SqliteParameter("originalDoc", feedModel.OriginalDocument ?? string.Empty)
2023-05-21 21:56:37 +02:00
}
};
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
if (reader.Read())
resultModel = ReaderToFeedModel(reader);
2023-05-18 01:27:11 +02:00
_sqlConn.Close();
return resultModel;
2023-05-18 01:27:11 +02:00
}
2023-05-22 13:26:27 +02:00
public async Task<bool> RemoveFeedAsync(FeedModel feedModel)
{
bool result = false;
_sqlConn.Open(); // After removing the feed unset the feed id from the feed items
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedTable} WHERE id=@id; UPDATE {_feedItemTable} SET feed_id=NULL WHERE feed_id=@id", _sqlConn)
2023-05-22 13:26:27 +02:00
{
Parameters =
{
new SqliteParameter("id", feedModel.Id)
}
};
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
_sqlConn.Close();
return result;
}
2023-05-22 15:55:21 +02:00
// Feed items
2023-05-24 19:27:22 +02:00
public async Task<HashSet<FeedItemModel>> GetFeedItemsAsync(string[]? feedIds = null)
2023-05-22 13:26:27 +02:00
{
List<string>? formattedIds = feedIds?.Select(s => $"'{s}'").ToList();
2023-05-24 19:27:22 +02:00
HashSet<FeedItemModel> feedItems = new HashSet<FeedItemModel>();
2023-05-22 13:26:27 +02:00
_sqlConn.Open();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand(
formattedIds != null
? $"SELECT * FROM {_feedItemTable} WHERE feed_id IN ({string.Join(", ", formattedIds)})"
2023-05-24 19:27:22 +02:00
: $"SELECT * FROM {_feedItemTable}", _sqlConn);
2023-05-22 13:26:27 +02:00
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
while (reader.Read())
{
2023-05-24 19:27:22 +02:00
feedItems.Add(new FeedItemModel()
2023-05-22 13:26:27 +02:00
{
Id = reader["id"].ToString(),
FeedId = reader["feed_id"].ToString(),
2023-05-24 19:27:22 +02:00
Read = int.TryParse(reader["read"].ToString(), out int parsedValue) && parsedValue != 0,
2023-05-22 13:26:27 +02:00
Title = reader["title"].ToString(),
Description = reader["description"].ToString(),
Link = reader["link"].ToString(),
LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())),
PublishingDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publishing_date"].ToString())),
Author = reader["author"].ToString(),
Categories = reader["categories"].ToString().Split(','),
Content = reader["content"].ToString()
});
}
_sqlConn.Close();
2023-05-24 19:27:22 +02:00
return feedItems;
2023-05-22 13:26:27 +02:00
}
public async Task<int> SetFeedItemsAsync(HashSet<FeedItemModel> items)
{
int result = 0;
_sqlConn.Open();
await using SqliteTransaction transaction = _sqlConn.BeginTransaction();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedItemTable} (id, feed_id, read, title, description, link, last_updated, publishing_date, author, categories, content)" +
$"VALUES (IFNULL((SELECT id FROM {_feedItemTable} WHERE link=@link), @id), @feedId, @read, @title, @description, @link, @lastUpdated, @publishingDate, @author, @categories, @content)", _sqlConn);
2023-05-22 13:26:27 +02:00
foreach (FeedItemModel item in items)
{
cmd.Parameters.Clear();
cmd.Parameters.Add(new SqliteParameter("id", item.Id ?? string.Empty));
cmd.Parameters.Add(new SqliteParameter("feedId", item.FeedId ?? string.Empty));
2023-05-22 13:26:27 +02:00
cmd.Parameters.Add(new SqliteParameter("read", item.Read ? 1 : 0));
cmd.Parameters.Add(new SqliteParameter("type", item.Type ?? string.Empty));
cmd.Parameters.Add(new SqliteParameter("title", item.Title ?? string.Empty));
cmd.Parameters.Add(new SqliteParameter("description", item.Description ?? string.Empty));
cmd.Parameters.Add(new SqliteParameter("link", item.Link ?? string.Empty));
2023-05-24 19:27:22 +02:00
cmd.Parameters.Add(new SqliteParameter("lastUpdated", item.LastUpdated?.ToUnixTimeMilliseconds()));
2023-05-22 13:26:27 +02:00
cmd.Parameters.Add(new SqliteParameter("publishingDate", item.PublishingDate?.ToUnixTimeMilliseconds() ?? 0));
cmd.Parameters.Add(new SqliteParameter("author", item.Author ?? string.Empty));
cmd.Parameters.Add(new SqliteParameter("categories", item.Categories != null ? string.Join(',', item.Categories) : string.Empty));
cmd.Parameters.Add(new SqliteParameter("content", item.Content ?? string.Empty));
if (_sqlConn.State != ConnectionState.Open)
_sqlConn.Open();
2023-05-22 13:26:27 +02:00
int affected = await cmd.ExecuteNonQueryAsync();
if (affected == 0)
Log.Verbose("Could not set feed item: {FeedLink}", item.Link);
2023-05-22 13:26:27 +02:00
else
result += affected;
}
transaction.Commit();
2023-05-22 13:26:27 +02:00
_sqlConn.Close();
return result; // Return the amount affected rows.
}
public async Task<bool> RemoveFeedItemAsync(FeedItemModel itemModel)
{
bool result = false;
_sqlConn.Open();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedItemTable} WHERE id=@id", _sqlConn)
2023-05-22 13:26:27 +02:00
{
Parameters =
{
new SqliteParameter("id", itemModel.Id)
}
};
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
_sqlConn.Close();
return result;
}
2023-05-24 19:27:22 +02:00
public async Task<GroupModel?> GetGroupFromFeedItemAsync(FeedItemModel feedItem)
{
GroupModel? result = null;
_sqlConn.Open();
2023-05-24 19:27:22 +02:00
await using SqliteCommand cmd = new SqliteCommand($"SELECT * FROM {_groupTable} WHERE id=(SELECT group_id FROM {_feedTable} WHERE id=@fId)", _sqlConn)
{
Parameters =
{
new SqliteParameter ("fId", feedItem.Id)
}
};
2023-05-24 19:27:22 +02:00
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
HashSet<GroupModel>? groups = null;
if (reader.Read())
groups = await GetGroupsAsync(reader["group_id"].ToString());
if (groups != null && groups.Any())
result = groups.FirstOrDefault();
_sqlConn.Close();
return result;
}
2023-05-18 01:27:11 +02:00
private async void InitializeDb()
{
Log.Verbose("Checking database...");
HashSet<string> failed = new HashSet<string>();
2023-05-18 01:27:11 +02:00
_sqlConn.Open();
2023-05-20 00:04:45 +02:00
Log.Verbose("Checking table: {Table}", _groupTable);
2023-05-21 21:56:37 +02:00
var queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_groupTable} (name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING, id STRING PRIMARY KEY)");
if (queryResponse.Any()) failed.Add("category_data");
2023-05-20 00:04:45 +02:00
Log.Verbose("Checking table: {Table}", _feedTable);
queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedTable} (id STRING PRIMARY KEY, url STRING NOT NULL, title STRING, group_id STRING, feed_type STRING, description STRING, language STRING, copyright STRING, date_added INT, last_updated INT, image_url STRING, original_document STRING)");
if (queryResponse.Any()) failed.Add("feed_data");
2023-05-20 00:04:45 +02:00
Log.Verbose("Checking table: {Table}", _feedItemTable);
queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedItemTable} (id STRING PRIMARY KEY, feed_id STRING, read INT, title STRING, description STRING, link STRING, last_updated INT, publishing_date INT, author STRING, categories STRING, content STRING)");
2023-05-20 00:04:45 +02:00
if (queryResponse.Any()) failed.Add("feed_item_data");
_sqlConn.Close();
if (failed.Any())
2023-05-18 01:27:11 +02:00
{
var joined = string.Join(',', failed);
Log.Error("Failed to initialize table(s): {TableNames}", joined);
2023-05-18 01:27:11 +02:00
}
else
Log.Verbose("Checking database done!");
2023-05-18 01:27:11 +02:00
}
public void Dispose()
{
_sqlConn.Dispose();
}
}
}