diff --git a/SharpRss/DbAccess.cs b/SharpRss/DbAccess.cs new file mode 100644 index 0000000..31a8e7f --- /dev/null +++ b/SharpRss/DbAccess.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using Microsoft.Data.Sqlite; +using Serilog; +using SharpRss.Models; + +namespace SharpRss +{ + internal static class DbAccess + { + static DbAccess() + { + Initialize(); + } + private static readonly string ConnectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.sqlite")};"; + private static bool _isInitialized; + + public static async Task SetSyndicationAsync(SyndicationContainer synContainer) + { + + } + + public static async Task> GetCategoriesAsync() + { + HashSet categories = new HashSet(); + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + await using DbDataReader reader = await dbc.ExecuteReaderAsync("SELECT * FROM category"); + while (await reader.ReadAsync()) + { + CategoryModel categoryModel = new CategoryModel() + { + Id = reader["id"].ToString(), + Name = reader["name"].ToString(), + HexColor = reader["hex_color"].ToString(), + FeedCount = 0, // Not implemented. + Icon = reader["icon"].ToString() + }; + categories.Add(categoryModel); + } + return categories; + } + public static async Task SetCategoryAsync(CategoryModel category) + { + if (category == null) return false; + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + int affected = await dbc.ExecuteAsync("INSERT OR REPLACE INTO category (id, hex_color, icon, name) VALUES (IFNULL((SELECT id FROM category WHERE name=@Name), @Id), @HexColor, @Icon, @Name)", + new { category.Id, category.HexColor, category.Icon, category.Name }); + return affected > 0; + } + public static async Task DeleteCategory(CategoryModel category) + { + if (category == null) return false; + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + int affected = await dbc.ExecuteAsync("DELETE FROM category WHERE id=@Id; UPDATE feed SET category_id=NULL WHERE category_id=@Id",new { category.Id }); + return affected > 0; + } + + public static async Task SetFeedAsync(FeedModel feed) + { + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + int affected = await dbc.ExecuteAsync("INSERT OR REPLACE INTO feed (url, title, category_id, feed_type, feed_version, description, language, copyright, publication_date, last_updated, categories, image_url) VALUES (@Url, @Title, @CategoryId, @FeedType, @FeedVersion, @Description, @language, @Copyright, @PublicationDate, @LastUpdated, @Categories, @ImageUrl)", new + { + Url = feed.OriginalUrl, + Title = feed.Title, + CategoryId = feed.CategoryId, + FeedType = feed.FeedType, + Description = feed.Description, + Language = feed.Language, + Copyright = feed.Copyright, + PublicationDate = feed.PublicationDate?.ToUnixTimeMilliseconds() ?? 0, + LastUpdated = feed.LastUpdated?.ToUnixTimeMilliseconds() ?? 0, + Categories = string.Join(',', feed.Categories), + ImageUrl = feed.ImageUrl + }); + if (affected == 0) + Log.Warning("Failed to add feed: {FeedUrl}", feed.OriginalUrl); + } + public static async Task> GetFeedsAsync(string[]? categoryIds = null) + { + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + HashSet feeds = new HashSet(); + await using DbDataReader reader = await dbc.ExecuteReaderAsync(categoryIds == null ? "SELECT * FROM feed" : "SELECT * FROM feed WHERE category_id IN(@CatIds)", new { CatIds = categoryIds }); + while (await reader.ReadAsync()) + { + FeedModel feedModel = new FeedModel() + { + OriginalUrl = reader["url"].ToString(), + Title = reader["title"].ToString(), + CategoryId = reader["category_id"].ToString(), + FeedType = reader["feed_type"].ToString(), + FeedVersion = reader["feed_version"].ToString(), + Description = reader["description"].ToString(), + Language = reader["language"].ToString(), + Copyright = reader["copyright"].ToString(), + PublicationDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publication_date"].ToString())), + LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())), + Categories = reader["categories"].ToString().Split(','), + ImageUrl = reader["image_url"].ToString() + }; + feeds.Add(feedModel); + } + return feeds; + } + + public static async Task SetFeedItemsAsync(HashSet items) + { + } + public static async Task> GetFeedItemsAsync(string[]? feedUrls) + { + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + HashSet items = new HashSet(); + await using DbDataReader reader = await dbc.ExecuteReaderAsync(feedUrls == null ? "SELECT * FROM feed_item" : "SELECT * FROM feed_item WHERE feed_item.feed_url IN(@Urls)", new { Urls = feedUrls }); + while (await reader.ReadAsync()) + { + FeedItemModel feedItemModel = new FeedItemModel() + { + Id = reader["id"].ToString(), + FeedUrl = reader["feed_url"].ToString(), + Read = int.TryParse(reader["read"].ToString(), out int parsedValue) && parsedValue != 0, + 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())), + Authors = reader["authors"].ToString().ToString().Split(','), + Categories = reader["categories"].ToString().Split(','), + Content = reader["content"].ToString() + }; + items.Add(feedItemModel); + } + return items; + } + + private static async void Initialize() + { + if (_isInitialized) return; + Log.Verbose("Checking database..."); + await using SqliteConnection dbc = new SqliteConnection(ConnectionString); + dbc.Open(); + Log.Verbose("Checking table: {Table}", "category"); + await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS category (id STRING PRIMARY KEY, name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING)"); + + Log.Verbose("Checking table: {Table}", "feed"); + await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS feed (url STRING PRIMARY KEY, title STRING, category_id STRING, feed_type STRING, feed_version STRING, description STRING, language STRING, copyright STRING, publication_date INT, last_updated INT, categories STRING, image_url STRING)"); + + Log.Verbose("Checking table: {Table}", "feed_item"); + await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS feed_item (id STRING PRIMARY KEY, feed_url STRING, read INT, title STRING, description STRING, link STRING, last_updated INT, publishing_date INT, authors STRING, categories STRING, content STRING)"); + Log.Verbose("Checking database done!"); + _isInitialized = true; + } + } +} \ No newline at end of file diff --git a/SharpRss/DbAccess_Old.cs b/SharpRss/DbAccess_Old.cs index e614774..520e18c 100644 --- a/SharpRss/DbAccess_Old.cs +++ b/SharpRss/DbAccess_Old.cs @@ -149,7 +149,7 @@ namespace SharpRss { new SqliteParameter("url", feedModel.OriginalUrl ?? string.Empty), new SqliteParameter("title", feedModel.Title ?? string.Empty), - new SqliteParameter("groupId", feedModel.GroupId ?? string.Empty), + new SqliteParameter("groupId", feedModel.CategoryId ?? string.Empty), new SqliteParameter("feedType", feedModel.FeedType ?? string.Empty), new SqliteParameter("description", feedModel.Description ?? string.Empty), new SqliteParameter("language", feedModel.Language ?? string.Empty), @@ -268,7 +268,7 @@ namespace SharpRss FeedItemModel feedItemModel = new FeedItemModel() { Id = reader["id"].ToString(), - FeedId = reader["feed_id"].ToString(), + FeedUrl = reader["feed_url"].ToString(), Read = int.TryParse(reader["read"].ToString(), out int parsedValue) && parsedValue != 0, Title = reader["title"].ToString(), Description = reader["description"].ToString(), @@ -281,11 +281,6 @@ namespace SharpRss Categories = reader["categories"].ToString().Split(','), Content = reader["content"].ToString() }; - if (feedItemModel is { FeedId: { } }) - { - FeedModel? feedModel = await GetFeedAsync(feedItemModel.FeedUrl); - //feedItemModel.Feed = feedModel; - } feedItems.Add(feedItemModel); } return feedItems; @@ -359,8 +354,8 @@ namespace SharpRss }; await using SqliteDataReader reader = await cmd.ExecuteReaderAsync(); HashSet? groups = null; - if (reader.Read()) - groups = await GetCategoriesAsync(reader["group_id"].ToString()); + /*if (reader.Read()) + groups = await GetCategoriesAsync(reader["group_id"].ToString());*/ if (groups != null && groups.Any()) result = groups.FirstOrDefault(); return result; @@ -371,7 +366,7 @@ namespace SharpRss { OriginalUrl = reader["url"].ToString(), Title = reader["title"].ToString(), - GroupId = reader["group_id"].ToString(), + CategoryId = reader["group_id"].ToString(), FeedType = reader["feed_type"].ToString(), Description = reader["description"].ToString(), Language = reader["language"].ToString(), diff --git a/SharpRss/Models/CategoryModel.cs b/SharpRss/Models/CategoryModel.cs index 3a6e736..6ab5ea7 100644 --- a/SharpRss/Models/CategoryModel.cs +++ b/SharpRss/Models/CategoryModel.cs @@ -1,20 +1,35 @@ using System; using ToolQit; +using ToolQit.Extensions; namespace SharpRss.Models { public class CategoryModel { - public CategoryModel() + private string _id = string.Empty; + public string Id { - HexColor = Utilities.GenerateRandomHexColor(); - Id = Guid.NewGuid().ToString(); + get + { + if (_id.IsNullEmptyWhiteSpace()) + _id = Guid.NewGuid().ToString(); + return _id; + } + set => _id = value; } - public string Name { get; set; } = string.Empty; - public string HexColor { get; set; } + private string _hexColor = string.Empty; + public string HexColor + { + get + { + if (_hexColor.IsNullEmptyWhiteSpace()) + _hexColor = Utilities.GenerateRandomHexColor(); + return _hexColor; + } + set => _hexColor = value; + } public int FeedCount { get; set; } public string Icon { get; set; } = string.Empty; - public string Id { get; set; } } } diff --git a/SharpRss/Models/FeedItemModel.cs b/SharpRss/Models/FeedItemModel.cs index bc7456f..4ed5b20 100644 --- a/SharpRss/Models/FeedItemModel.cs +++ b/SharpRss/Models/FeedItemModel.cs @@ -5,8 +5,6 @@ namespace SharpRss.Models public class FeedItemModel { public string? Id { get; set; } = string.Empty; - // FeedId will be removed - public string? FeedId { get; set; } = string.Empty; public string FeedUrl { get; set; } = string.Empty; public bool Read { get; set; } public string? Type { get; set; } = string.Empty; diff --git a/SharpRss/Models/FeedModel.cs b/SharpRss/Models/FeedModel.cs index 0e761e1..df187c2 100644 --- a/SharpRss/Models/FeedModel.cs +++ b/SharpRss/Models/FeedModel.cs @@ -6,10 +6,9 @@ namespace SharpRss.Models { public class FeedModel { - public CategoryModel? Category { get; set; } public string OriginalUrl { get; set; } = string.Empty; public string? Title { get; set; } = string.Empty; - public string? GroupId { get; set; } = string.Empty; + public string? CategoryId { get; set; } = string.Empty; public string? FeedType { get; set; } = string.Empty; public string? FeedVersion { get; set; } = string.Empty; public string? Description { get; set; } = string.Empty; diff --git a/SharpRss/Services/RssService.cs b/SharpRss/Services/RssService.cs index 0e544b2..bbb265d 100644 --- a/SharpRss/Services/RssService.cs +++ b/SharpRss/Services/RssService.cs @@ -11,7 +11,7 @@ namespace SharpRss.Services /// /// Managing RSS feeds and groups. /// - public class RssService : IDisposable + public class RssService { public RssService() { @@ -26,28 +26,17 @@ namespace SharpRss.Services return items; } public async Task CreateGroupAsync(CategoryModel group) => await DbAccess_Old.SetCategoryAsync(group); - public async Task> GetCategoriesAsync() => await DbAccess_Old.GetCategoriesAsync(); + public async Task> GetCategoriesAsync() => await DbAccess.GetCategoriesAsync(); //TODO: Rework this! // Subscribe to a feed. - public async Task AddSubscriptionAsync(string url, CategoryModel? group = null) + public async Task AddSubscriptionAsync(string url, CategoryModel? category = null) { - /*if (!SyndicationManager.GetFeed(url, out GenericSyndicationFeed? genFeed)) return false;*/ - var feed = SyndicationManager.CreateSyndication(url); - // Check if feed exists in db - FeedModel? fModel = await DbAccess_Old.GetFeedAsync(url); - // If not exists fetch feed & add. - if (fModel == null) - { - /*if (!SyndicationManager.TryGetGenericFeed(url, out GenericSyndicationFeed? genFeed)) return false;*/ - /*if (genFeed == null) return false; - fModel = FromResource(genFeed.Resource); - fModel.GroupId = group?.Id;*/ - // Add feed - //FeedModel? dbFeed = await DbAccess.SetFeedAsync(fModel); - // Update/fetch items - //await DbAccess.FetchFeedItemsAsync(new string[] { fModel.Url }); - } + var syndication = SyndicationManager.CreateSyndication(url); + if (category != null) + syndication.Category = category; + if (!syndication.Fetched) return false; + return true; } private static FeedModel FromResource(ISyndicationResource resource) @@ -62,7 +51,7 @@ namespace SharpRss.Services model.Description = rssFeed.Channel.Description; model.Copyright = rssFeed.Channel.Copyright; model.OriginalUrl = rssFeed.Channel.SelfLink.ToString(); - model.ImageUrl = rssFeed.Channel.Image?.Url.ToString(); + model.ImageUrl = rssFeed.Channel.Image?.Url.ToString() ?? string.Empty; model.Language = rssFeed.Channel.Language?.ToString(); break; case SyndicationContentFormat.Atom: @@ -81,13 +70,13 @@ namespace SharpRss.Services var feeds = await GetFeedsAsync(); } - public async Task> GetFeedsAsync(string? groupId = null) => await DbAccess_Old.GetFeedsAsync(groupId); - public async Task> GetUngroupedFeedsAsync() => await DbAccess_Old.GetFeedsAsync(""); + public async Task> GetFeedsAsync(string? groupId = null) => await DbAccess.GetFeedsAsync(); + public async Task> GetUngroupedFeedsAsync() => await DbAccess.GetFeedsAsync(); public async Task> GetFeedItemsAsync(string feedId, string? groupId = null) => await GetFeedItemsFromFeedsAsync(new[] { feedId }, groupId); public async Task> GetFeedItemsFromFeedsAsync(string[] feedIds, string? groupId = null) { - var items = await DbAccess_Old.GetFeedItemsAsync(feedIds); + var items = await DbAccess.GetFeedItemsAsync(feedIds); return items; } @@ -140,7 +129,9 @@ namespace SharpRss.Services } private async void SetupTestGroupsAndFeedsAsync() { - await AddSubscriptionAsync("https://en.wikipedia.org/w/api.php?hidebots=1&hidecategorization=1&hideWikibase=1&urlversion=1&days=7&limit=50&action=feedrecentchanges&feedformat=atom"); + /*CategoryModel newModel = new CategoryModel() { Name = "Test" }; + bool added = await DbAccess.SetCategoryAsync(newModel);*/ + //await AddSubscriptionAsync("https://en.wikipedia.org/w/api.php?hidebots=1&hidecategorization=1&hideWikibase=1&urlversion=1&days=7&limit=50&action=feedrecentchanges&feedformat=atom"); //TODO: Make multiple adding of feed to a transaction, now throws an exception. /*var groupRes = await CreateGroupAsync(new GroupModel() { Name = "Test" }); groupRes = await CreateGroupAsync(new GroupModel() { Name = "News" }); @@ -164,7 +155,7 @@ namespace SharpRss.Services }*/ /*var groups = await GetGroupsAsync(); CategoryModel testGroup = groups.Single(x => x.Name == "News");*/ - + /*await AddFeedsAsync(new[] { "https://www.nu.nl/rss/Algemeen", @@ -173,9 +164,5 @@ namespace SharpRss.Services "http://news.google.com/?output=atom" }, testGroup);*/ } - - public void Dispose() - { - } } } \ No newline at end of file diff --git a/SharpRss/SyndicationManager.cs b/SharpRss/SyndicationManager.cs index d125459..09ebbcb 100644 --- a/SharpRss/SyndicationManager.cs +++ b/SharpRss/SyndicationManager.cs @@ -10,15 +10,13 @@ using SharpRss.Models; namespace SharpRss { - /// - /// Struct that contains the necessary objects for adding/fetching feeds and items - /// public struct SyndicationContainer { public GenericSyndicationFeed SyndicationFeed { get; set; } public CategoryModel Category { get; set; } public FeedModel FeedModel { get; set; } public HashSet FeedItems { get; set; } + public bool Fetched; } public static class SyndicationManager { @@ -127,6 +125,7 @@ namespace SharpRss Log.Warning("Feed implementation missing!"); break; } + container.Fetched = true; return container; } } diff --git a/WebSharpRSS/Bootstrapper.cs b/WebSharpRSS/Bootstrapper.cs index 5a1fc93..6b7f094 100644 --- a/WebSharpRSS/Bootstrapper.cs +++ b/WebSharpRSS/Bootstrapper.cs @@ -19,7 +19,7 @@ namespace WebSharpRSS Caretaker.Settings.SetAppDefaultSettings(); SetupLogging(); Log.Information("Starting SharpRSS..."); - DbAccess_Old.Initialize(); + //DbAccess_Old.Initialize(); _bootstrapped = true; } diff --git a/WebSharpRSS/WebSharpRSS.csproj b/WebSharpRSS/WebSharpRSS.csproj index e998e8b..584f102 100644 --- a/WebSharpRSS/WebSharpRSS.csproj +++ b/WebSharpRSS/WebSharpRSS.csproj @@ -40,6 +40,9 @@ <_ContentIncludedByDefault Remove="logs\log_20230526.json" /> <_ContentIncludedByDefault Remove="logs\log_20230527.json" /> <_ContentIncludedByDefault Remove="logs\log_20230529.json" /> + <_ContentIncludedByDefault Remove="logs\log_20230602.json" /> + <_ContentIncludedByDefault Remove="logs\log_20230603.json" /> + <_ContentIncludedByDefault Remove="logs\log_20230604.json" /> diff --git a/WebSharpRSS/sharp_rss.sqlite b/WebSharpRSS/sharp_rss.sqlite index e078705..f905b33 100644 Binary files a/WebSharpRSS/sharp_rss.sqlite and b/WebSharpRSS/sharp_rss.sqlite differ