Start cleaning up account types

This commit is contained in:
2025-11-15 15:30:23 -05:00
parent 6bae35a255
commit 66ea567eaa
23 changed files with 82 additions and 88 deletions

View File

@@ -1,218 +0,0 @@
using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Realms;
namespace FeedCenter.Feeds;
public class Account : RealmObject, INotifyDataErrorInfo
{
public const string DefaultName = "< Local >";
private readonly DataErrorDictionary _dataErrorDictionary;
public Account() : this(AccountType.Local)
{
}
public Account(AccountType type)
{
Type = type;
_dataErrorDictionary = new DataErrorDictionary();
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
}
[PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
public AccountType Type
{
get => Enum.TryParse(TypeRaw, out AccountType result) ? result : AccountType.Local;
set => TypeRaw = value.ToString();
}
public bool SupportsFeedEdit => Type switch
{
AccountType.Fever => false,
AccountType.GoogleReader => false,
AccountType.Miniflux => true,
AccountType.Local => true,
_ => throw new NotSupportedException()
};
public bool SupportsFeedDelete => Type switch
{
AccountType.Fever => false,
AccountType.GoogleReader => false,
AccountType.Miniflux => true,
AccountType.Local => true,
_ => throw new NotSupportedException()
};
private string TypeRaw { get; set; }
public string Name
{
get => RawName;
set
{
RawName = value;
ValidateString(nameof(Name), RawName);
RaisePropertyChanged();
}
}
[MapTo("Name")]
private string RawName { get; set; } = string.Empty;
public string Url
{
get => RawUrl;
set
{
RawUrl = value;
ValidateString(nameof(Url), RawUrl);
RaisePropertyChanged();
}
}
[MapTo("Url")]
public string RawUrl { get; set; }
public string Username
{
get => RawUsername;
set
{
RawUsername = value;
if (!Authenticate)
{
_dataErrorDictionary.ClearErrors(nameof(Username));
return;
}
ValidateString(nameof(Username), RawUsername);
RaisePropertyChanged();
}
}
[MapTo("Username")]
public string RawUsername { get; set; }
public string Password
{
get => RawPassword;
set
{
RawPassword = value;
if (!Authenticate)
{
_dataErrorDictionary.ClearErrors(nameof(Password));
return;
}
ValidateString(nameof(Password), RawPassword);
RaisePropertyChanged();
}
}
[MapTo("Password")]
public string RawPassword { get; set; }
public bool Authenticate { get; set; }
public bool Enabled { get; set; } = true;
public int CheckInterval { get; set; } = 60;
public DateTimeOffset LastChecked { get; set; }
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
{
return _dataErrorDictionary.GetErrors(propertyName);
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
private void ValidateString(string propertyName, string value)
{
_dataErrorDictionary.ClearErrors(propertyName);
if (string.IsNullOrWhiteSpace(value))
_dataErrorDictionary.AddError(propertyName, $"{propertyName} cannot be empty");
}
public static Account CreateDefault()
{
return new Account { Name = DefaultName, Type = AccountType.Local };
}
public async Task<int> GetProgressSteps(Account account, AccountReadInput accountReadInput)
{
var progressSteps = Type switch
{
// Delegate to the right reader based on the account type
AccountType.Fever => await new FeverReader(account).GetProgressSteps(accountReadInput),
AccountType.GoogleReader => await new GoogleReaderReader(account).GetProgressSteps(accountReadInput),
AccountType.Miniflux => await new MinifluxReader(account).GetProgressSteps(accountReadInput),
AccountType.Local => await new LocalReader(account).GetProgressSteps(accountReadInput),
_ => throw new NotSupportedException()
};
return progressSteps;
}
public async Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
// If not enabled then do nothing
if (!Enabled)
return AccountReadResult.NotEnabled;
// Check if we're forcing a read
if (!accountReadInput.ForceRead)
{
// Figure out how long since we last checked
var timeSpan = DateTimeOffset.Now - LastChecked;
// Check if we are due to read the feed
if (timeSpan.TotalMinutes < CheckInterval)
return AccountReadResult.NotDue;
}
AccountReadResult accountReadResult;
switch (Type)
{
// Delegate to the right reader based on the account type
case AccountType.Fever:
accountReadResult = await new FeverReader(this).Read(accountReadInput);
break;
case AccountType.GoogleReader:
accountReadResult = await new GoogleReaderReader(this).Read(accountReadInput);
break;
case AccountType.Miniflux:
accountReadResult = await new MinifluxReader(this).Read(accountReadInput);
break;
case AccountType.Local:
accountReadResult = await new LocalReader(this).Read(accountReadInput);
break;
default:
throw new NotSupportedException();
}
return accountReadResult;
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace FeedCenter.Feeds;
public class AccountReadInput(FeedCenterEntities entities, Guid? feedId, bool forceRead, Action incrementProgress)
{
public FeedCenterEntities Entities { get; set; } = entities;
public Guid? FeedId { get; set; } = feedId;
public bool ForceRead { get; set; } = forceRead;
public Action IncrementProgress { get; private set; } = incrementProgress ?? throw new ArgumentNullException(nameof(incrementProgress));
}

View File

@@ -1,8 +0,0 @@
namespace FeedCenter.Feeds;
public enum AccountReadResult
{
Success,
NotDue,
NotEnabled
}

View File

@@ -1,9 +0,0 @@
namespace FeedCenter.Feeds;
public enum AccountType
{
Local,
Fever,
GoogleReader,
Miniflux
}

View File

@@ -2,6 +2,7 @@
using System.Collections;
using System.ComponentModel;
using System.Linq;
using FeedCenter.Accounts;
using JetBrains.Annotations;
using Realms;

View File

@@ -12,6 +12,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ChrisKaczor.ApplicationUpdate;
using FeedCenter.Accounts;
using FeedCenter.FeedParsers;
using FeedCenter.Properties;
using FeedCenter.Xml;

View File

@@ -1,8 +1,8 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FeedCenter.Accounts;
using FeedCenter.Options;
using Realms;
@@ -86,24 +86,7 @@ public partial class FeedItem : RealmObject
if (feed == null || feed.Account.Type == AccountType.Local)
return;
switch (feed.Account.Type)
{
case AccountType.Fever:
// Delegate to the right reader based on the account type
await new FeverReader(feed.Account).MarkFeedItemRead(RemoteId);
break;
case AccountType.Miniflux:
// Delegate to the right reader based on the account type
await new MinifluxReader(feed.Account).MarkFeedItemRead(RemoteId);
break;
case AccountType.Local:
break;
default:
Debug.Assert(false);
break;
}
await AccountReaderFactory.CreateAccountReader(feed.Account).MarkFeedItemRead(RemoteId);
}
[GeneratedRegex("\\n")]

View File

@@ -1,151 +0,0 @@
using ChrisKaczor.FeverClient;
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace FeedCenter.Feeds;
internal class FeverReader(Account account) : IAccountReader
{
public async Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
var feverClient = new FeverClient(account.Url, apiKey);
var feeds = await feverClient.GetFeeds();
return feeds.Count() * 2 + 5;
}
public async Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
var checkTime = DateTimeOffset.UtcNow;
var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
var feverClient = new FeverClient(account.Url, apiKey);
accountReadInput.IncrementProgress();
var feverFeeds = (await feverClient.GetFeeds()).ToList();
accountReadInput.IncrementProgress();
var allFeverFeedItems = (await feverClient.GetAllFeedItems()).ToList();
accountReadInput.IncrementProgress();
var transaction = accountReadInput.Entities.BeginTransaction();
foreach (var feverFeed in feverFeeds)
{
var feed = accountReadInput.Entities.Feeds.FirstOrDefault(f => f.RemoteId == feverFeed.Id.ToString() && f.Account.Id == account.Id);
if (feed == null)
{
feed = new Feed
{
Id = Guid.NewGuid(),
RemoteId = feverFeed.Id.ToString(),
Title = feverFeed.Title,
Source = feverFeed.Url,
Link = feverFeed.SiteUrl,
Account = account,
Name = feverFeed.Title,
CategoryId = accountReadInput.Entities.DefaultCategory.Id,
Enabled = true,
CheckInterval = 0,
};
accountReadInput.Entities.Feeds.Add(feed);
}
feed.Name = feverFeed.Title;
feed.Title = feverFeed.Title;
feed.Link = feverFeed.SiteUrl;
feed.Source = feverFeed.Url;
feed.LastReadResult = FeedReadResult.Success;
feed.LastChecked = checkTime;
accountReadInput.IncrementProgress();
var feverFeedItems = allFeverFeedItems
.Where(f => f.FeedId == feverFeed.Id)
.OrderByDescending(fi => fi.CreatedOnTime).ToList();
var sequence = 1;
foreach (var feverFeedItem in feverFeedItems)
{
var feedItem = feed.Items.FirstOrDefault(f => f.RemoteId == feverFeedItem.Id.ToString());
if (feedItem == null)
{
feedItem = new FeedItem
{
Id = Guid.NewGuid(),
RemoteId = feverFeedItem.Id.ToString(),
Title = feverFeedItem.Title,
Link = feverFeedItem.Url,
Description = feverFeedItem.Html,
BeenRead = feverFeedItem.IsRead,
FeedId = feed.Id,
Guid = Guid.NewGuid().ToString(),
Sequence = sequence++,
};
feed.Items.Add(feedItem);
}
feedItem.LastFound = checkTime;
feedItem.BeenRead = feverFeedItem.IsRead;
feedItem.Sequence = sequence++;
}
accountReadInput.IncrementProgress();
var feedItemsNotSeen = feed.Items.Where(fi => fi.LastFound != checkTime).ToList();
foreach (var feedItemNotSeen in feedItemsNotSeen)
{
feed.Items.Remove(feedItemNotSeen);
}
}
accountReadInput.IncrementProgress();
var feedsNotSeen = accountReadInput.Entities.Feeds.Where(f => f.Account.Id == account.Id && f.LastChecked != checkTime).ToList();
foreach (var feedNotSeen in feedsNotSeen)
{
accountReadInput.Entities.Feeds.Remove(feedNotSeen);
}
account.LastChecked = checkTime;
await transaction.CommitAsync();
accountReadInput.IncrementProgress();
return AccountReadResult.Success;
}
public async Task MarkFeedItemRead(string feedItemId)
{
var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
var feverClient = new FeverClient(account.Url, apiKey);
await feverClient.MarkFeedItemAsRead(int.Parse(feedItemId));
}
private static string GetApiKey(Account account)
{
var input = $"{account.Username}:{account.Password}";
var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}

View File

@@ -1,143 +0,0 @@
using System;
using System.Threading.Tasks;
namespace FeedCenter.Feeds;
internal class GoogleReaderReader(Account account) : IAccountReader
{
public Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
return Task.FromResult(7);
}
public async Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
var checkTime = DateTimeOffset.UtcNow;
//var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
//var feverClient = new FeverClient.FeverClient(account.Url, apiKey);
//accountReadInput.IncrementProgress();
//var feverFeeds = feverClient.GetFeeds().Result.ToList();
//accountReadInput.IncrementProgress();
//var allFeverFeedItems = feverClient.GetAllFeedItems().Result.ToList();
//accountReadInput.IncrementProgress();
var transaction = accountReadInput.Entities.BeginTransaction();
//foreach (var feverFeed in feverFeeds)
//{
// var feed = accountReadInput.Entities.Feeds.FirstOrDefault(f => f.RemoteId == feverFeed.Id.ToString() && f.Account.Id == account.Id);
// if (feed == null)
// {
// feed = new Feed
// {
// Id = Guid.NewGuid(),
// RemoteId = feverFeed.Id.ToString(),
// Title = feverFeed.Title,
// Source = feverFeed.Url,
// Link = feverFeed.SiteUrl,
// Account = account,
// Name = feverFeed.Title,
// CategoryId = accountReadInput.Entities.DefaultCategory.Id,
// Enabled = true,
// CheckInterval = 0,
// };
// accountReadInput.Entities.Feeds.Add(feed);
// }
// feed.Name = feverFeed.Title;
// feed.Title = feverFeed.Title;
// feed.Link = feverFeed.SiteUrl;
// feed.Source = feverFeed.Url;
// feed.LastReadResult = FeedReadResult.Success;
// feed.LastChecked = checkTime;
// accountReadInput.IncrementProgress();
// var feverFeedItems = allFeverFeedItems
// .Where(f => f.FeedId == feverFeed.Id)
// .OrderByDescending(fi => fi.CreatedOnTime).ToList();
// var sequence = 1;
// foreach (var feverFeedItem in feverFeedItems)
// {
// var feedItem = feed.Items.FirstOrDefault(f => f.RemoteId == feverFeedItem.Id.ToString());
// if (feedItem == null)
// {
// feedItem = new FeedItem
// {
// Id = Guid.NewGuid(),
// RemoteId = feverFeedItem.Id.ToString(),
// Title = feverFeedItem.Title,
// Link = feverFeedItem.Url,
// Description = feverFeedItem.Html,
// BeenRead = feverFeedItem.IsRead,
// FeedId = feed.Id,
// Guid = Guid.NewGuid().ToString(),
// Sequence = sequence++,
// };
// feed.Items.Add(feedItem);
// }
// feedItem.LastFound = checkTime;
// feedItem.BeenRead = feverFeedItem.IsRead;
// feedItem.Sequence = sequence++;
// }
// accountReadInput.IncrementProgress();
// var feedItemsNotSeen = feed.Items.Where(fi => fi.LastFound != checkTime).ToList();
// foreach (var feedItemNotSeen in feedItemsNotSeen)
// {
// feed.Items.Remove(feedItemNotSeen);
// }
//}
accountReadInput.IncrementProgress();
//var feedsNotSeen = accountReadInput.Entities.Feeds.Where(f => f.Account.Id == account.Id && f.LastChecked != checkTime).ToList();
//foreach (var feedNotSeen in feedsNotSeen)
//{
// accountReadInput.Entities.Feeds.Remove(feedNotSeen);
//}
account.LastChecked = checkTime;
await transaction.CommitAsync();
accountReadInput.IncrementProgress();
return AccountReadResult.Success;
}
public Task MarkFeedItemRead(string feedItemId)
{
//var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
//var feverClient = new FeverClient.FeverClient(account.Url, apiKey);
//await feverClient.MarkFeedItemAsRead(int.Parse(feedItemId));
return Task.CompletedTask;
}
//private static string GetApiKey(Account account)
//{
// var input = $"{account.Username}:{account.Password}";
// var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
// return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
//}
}

View File

@@ -1,10 +0,0 @@
using System.Threading.Tasks;
namespace FeedCenter.Feeds;
public interface IAccountReader
{
public Task<int> GetProgressSteps(AccountReadInput accountReadInput);
public Task<AccountReadResult> Read(AccountReadInput accountReadInput);
public Task MarkFeedItemRead(string feedItemId);
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace FeedCenter.Feeds;
public class LocalReader(Account account) : IAccountReader
{
public Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var enabledFeedCount = accountReadInput.Entities.Feeds.Count(f => f.Account.Type == AccountType.Local && f.Enabled);
return Task.FromResult(enabledFeedCount);
}
public Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
var checkTime = DateTimeOffset.UtcNow;
// Create the list of feeds to read
var feedsToRead = new List<Feed>();
// If we have a single feed then add it to the list - otherwise add them all
if (accountReadInput.FeedId != null)
feedsToRead.Add(accountReadInput.Entities.Feeds.First(feed => feed.Id == accountReadInput.FeedId));
else
feedsToRead.AddRange(accountReadInput.Entities.Feeds.Where(f => f.Account.Type == AccountType.Local));
// Loop over each feed and read it
foreach (var feed in feedsToRead)
{
// Read the feed
accountReadInput.Entities.SaveChanges(() => feed.Read(accountReadInput.ForceRead));
accountReadInput.IncrementProgress();
}
accountReadInput.Entities.SaveChanges(() => account.LastChecked = checkTime);
return Task.FromResult(AccountReadResult.Success);
}
public Task MarkFeedItemRead(string feedItemId)
{
throw new NotImplementedException();
}
}

View File

@@ -1,146 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using ChrisKaczor.MinifluxClient;
namespace FeedCenter.Feeds;
internal class MinifluxReader(Account account) : IAccountReader
{
public async Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var minifluxClient = new MinifluxClient(account.Url, account.Password);
int feedCount;
if (accountReadInput.FeedId.HasValue)
{
feedCount = 1;
}
else
{
var feeds = await minifluxClient.GetFeeds();
feedCount = feeds.Count();
}
return feedCount * 2 + 4;
}
public async Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
var checkTime = DateTimeOffset.UtcNow;
var minifluxClient = new MinifluxClient(account.Url, account.Password);
accountReadInput.IncrementProgress();
var localFeeds = accountReadInput.Entities.Feeds.ToList();
var remoteFeeds = (await minifluxClient.GetFeeds()).ToList();
if (accountReadInput.FeedId.HasValue)
{
localFeeds = localFeeds.Where(f => f.Id == accountReadInput.FeedId.Value).ToList();
remoteFeeds = remoteFeeds.Where(rf => rf.Id.ToString() == localFeeds.First().RemoteId).ToList();
}
accountReadInput.IncrementProgress();
var transaction = accountReadInput.Entities.BeginTransaction();
foreach (var remoteFeed in remoteFeeds)
{
var feed = accountReadInput.Entities.Feeds.FirstOrDefault(f => f.RemoteId == remoteFeed.Id.ToString() && f.Account.Id == account.Id);
if (feed == null)
{
feed = new Feed
{
Id = Guid.NewGuid(),
RemoteId = remoteFeed.Id.ToString(),
Title = remoteFeed.Title,
Source = remoteFeed.FeedUrl,
Link = remoteFeed.SiteUrl,
Account = account,
Name = remoteFeed.Title,
CategoryId = accountReadInput.Entities.DefaultCategory.Id,
Enabled = true,
CheckInterval = 0,
};
accountReadInput.Entities.Feeds.Add(feed);
}
feed.Name = remoteFeed.Title;
feed.Title = remoteFeed.Title;
feed.Link = remoteFeed.SiteUrl;
feed.Source = remoteFeed.FeedUrl;
feed.LastReadResult = FeedReadResult.Success;
feed.LastChecked = checkTime;
accountReadInput.IncrementProgress();
var sequence = 1;
var remoteFeedItems = (await minifluxClient.GetFeedEntries(remoteFeed.Id, SortField.PublishedAt, SortDirection.Descending)).ToList();
foreach (var remoteFeedItem in remoteFeedItems)
{
var feedItem = feed.Items.FirstOrDefault(f => f.RemoteId == remoteFeedItem.Id.ToString());
if (feedItem == null)
{
feedItem = new FeedItem
{
Id = Guid.NewGuid(),
RemoteId = remoteFeedItem.Id.ToString(),
Title = remoteFeedItem.Title,
Link = remoteFeedItem.Url,
Description = remoteFeedItem.Content,
BeenRead = remoteFeedItem.Status == "read",
FeedId = feed.Id,
Guid = Guid.NewGuid().ToString(),
Sequence = sequence++,
};
feed.Items.Add(feedItem);
}
feedItem.LastFound = checkTime;
feedItem.BeenRead = remoteFeedItem.Status == "read";
feedItem.Sequence = sequence++;
}
accountReadInput.IncrementProgress();
var feedItemsNotSeen = feed.Items.Where(fi => fi.LastFound != checkTime).ToList();
foreach (var feedItemNotSeen in feedItemsNotSeen)
{
feed.Items.Remove(feedItemNotSeen);
}
}
accountReadInput.IncrementProgress();
var feedsNotSeen = localFeeds.Where(f => f.Account.Id == account.Id && f.LastChecked != checkTime).ToList();
foreach (var feedNotSeen in feedsNotSeen)
{
accountReadInput.Entities.Feeds.Remove(feedNotSeen);
}
account.LastChecked = checkTime;
await transaction.CommitAsync();
accountReadInput.IncrementProgress();
return AccountReadResult.Success;
}
public async Task MarkFeedItemRead(string feedItemId)
{
var minifluxClient = new MinifluxClient(account.Url, account.Password);
await minifluxClient.MarkFeedEntries([long.Parse(feedItemId)], FeedEntryStatus.Read);
}
}