More UI updates

This commit is contained in:
2023-04-16 12:57:17 -04:00
parent 5c0c84a068
commit d6a2fd5a46
47 changed files with 3695 additions and 3768 deletions

View File

@@ -2,142 +2,141 @@
using System;
using System.Xml;
namespace FeedCenter.FeedParsers
namespace FeedCenter.FeedParsers;
internal class AtomParser : FeedParserBase
{
internal class AtomParser : FeedParserBase
public AtomParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
{
public AtomParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
try
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Load the XML document from the text
document.LoadXml(feedText);
// Get the root node
XmlNode rootNode = document.DocumentElement;
// Get the root node
XmlNode rootNode = document.DocumentElement;
// If we didn't find a root node then bail
if (rootNode == null)
return FeedReadResult.UnknownError;
// If we didn't find a root node then bail
if (rootNode == null)
return FeedReadResult.UnknownError;
// Initialize the sequence number for items
var sequence = 0;
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the root node
foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "title":
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
string rel = null;
if (node.Attributes == null)
break;
XmlNode relNode = GetAttribute(node, "rel");
if (relNode != null)
rel = relNode.InnerText;
if (string.IsNullOrEmpty(rel) || rel == "alternate")
Feed.Link = GetAttribute(node, "href").InnerText.Trim();
break;
case "subtitle":
Feed.Description = node.InnerText;
break;
case "entry":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml;
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
// Loop over all nodes in the root node
foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
switch (node.Name)
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "id":
feedItem.Guid = childNode.InnerText;
break;
case "content":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
string rel = null;
if (childNode.Attributes == null)
if (node.Attributes == null)
break;
XmlNode relNode = GetAttribute(childNode, "rel");
XmlNode relNode = GetAttribute(node, "rel");
if (relNode != null)
rel = relNode.InnerText.Trim();
rel = relNode.InnerText;
if (string.IsNullOrEmpty(rel) || rel == "alternate")
{
var link = GetAttribute(childNode, "href").InnerText;
Feed.Link = GetAttribute(node, "href").InnerText.Trim();
if (link.StartsWith("/"))
{
var uri = new Uri(Feed.Link);
break;
link = uri.Scheme + "://" + uri.Host + link;
}
feedItem.Link = link;
}
case "subtitle":
Feed.Description = node.InnerText;
break;
case "entry":
HandleFeedItem(node, ref sequence);
break;
}
}
if (string.IsNullOrWhiteSpace(feedItem.Guid))
feedItem.Guid = feedItem.Link;
return feedItem;
return FeedReadResult.Success;
}
private static XmlAttribute GetAttribute(XmlNode node, string attributeName)
catch (XmlException xmlException)
{
if (node?.Attributes == null)
return null;
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return node.Attributes[attributeName, node.NamespaceURI] ?? node.Attributes[attributeName];
return FeedReadResult.InvalidXml;
}
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "id":
feedItem.Guid = childNode.InnerText;
break;
case "content":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break;
case "link":
string rel = null;
if (childNode.Attributes == null)
break;
XmlNode relNode = GetAttribute(childNode, "rel");
if (relNode != null)
rel = relNode.InnerText.Trim();
if (string.IsNullOrEmpty(rel) || rel == "alternate")
{
var link = GetAttribute(childNode, "href").InnerText;
if (link.StartsWith("/"))
{
var uri = new Uri(Feed.Link);
link = uri.Scheme + "://" + uri.Host + link;
}
feedItem.Link = link;
}
break;
}
}
if (string.IsNullOrWhiteSpace(feedItem.Guid))
feedItem.Guid = feedItem.Link;
return feedItem;
}
private static XmlAttribute GetAttribute(XmlNode node, string attributeName)
{
if (node?.Attributes == null)
return null;
return node.Attributes[attributeName, node.NamespaceURI] ?? node.Attributes[attributeName];
}
}

View File

@@ -1,8 +1,7 @@
namespace FeedCenter.FeedParsers
namespace FeedCenter.FeedParsers;
internal enum FeedParseError
{
internal enum FeedParseError
{
Unknown = 0,
InvalidXml = 1
}
Unknown = 0,
InvalidXml = 1
}

View File

@@ -1,14 +1,13 @@
using System;
namespace FeedCenter.FeedParsers
{
internal class FeedParseException : ApplicationException
{
public FeedParseException(FeedParseError feedParseError)
{
ParseError = feedParseError;
}
namespace FeedCenter.FeedParsers;
public FeedParseError ParseError { get; set; }
internal class FeedParseException : ApplicationException
{
public FeedParseException(FeedParseError feedParseError)
{
ParseError = feedParseError;
}
public FeedParseError ParseError { get; set; }
}

View File

@@ -3,159 +3,158 @@ using System;
using System.Linq;
using System.Xml;
namespace FeedCenter.FeedParsers
namespace FeedCenter.FeedParsers;
[Serializable]
internal class InvalidFeedFormatException : ApplicationException
{
[Serializable]
internal class InvalidFeedFormatException : ApplicationException
internal InvalidFeedFormatException(Exception exception)
: base(string.Empty, exception)
{
internal InvalidFeedFormatException(Exception exception)
: base(string.Empty, exception)
{
}
}
internal abstract class FeedParserBase
{
#region Member variables
protected readonly Feed Feed;
#endregion
#region Constructor
protected FeedParserBase(Feed feed)
{
Feed = feed;
}
#endregion
#region Methods
public abstract FeedReadResult ParseFeed(string feedText);
protected abstract FeedItem ParseFeedItem(XmlNode node);
protected void HandleFeedItem(XmlNode node, ref int sequence)
{
// Build a feed item from the node
var newFeedItem = ParseFeedItem(node);
if (newFeedItem == null)
return;
// Check for feed items with no guid or link
if (string.IsNullOrWhiteSpace(newFeedItem.Guid) && string.IsNullOrWhiteSpace(newFeedItem.Link))
return;
// Look for an item that has the same guid
var existingFeedItem = Feed.Items.FirstOrDefault(item => item.Guid == newFeedItem.Guid && item.Id != newFeedItem.Id);
// Check to see if we already have this feed item
if (existingFeedItem == null)
{
Log.Logger.Information("New link: " + newFeedItem.Link);
// Associate the new item with the right feed
newFeedItem.Feed = Feed;
// Set the item as new
newFeedItem.New = true;
// Add the item to the list
Feed.Items.Add(newFeedItem);
// Feed was updated
Feed.LastUpdated = DateTime.Now;
}
else
{
Log.Logger.Information("Existing link: " + newFeedItem.Link);
// Update the fields in the existing item
existingFeedItem.Link = newFeedItem.Link;
existingFeedItem.Title = newFeedItem.Title;
existingFeedItem.Guid = newFeedItem.Guid;
existingFeedItem.Description = newFeedItem.Description;
// Item is no longer new
existingFeedItem.New = false;
// Switch over to the existing item for the rest
newFeedItem = existingFeedItem;
}
// Item was last seen now
newFeedItem.LastFound = Feed.LastChecked;
// Set the sequence
newFeedItem.Sequence = sequence;
// Increment the sequence
sequence++;
}
#endregion
#region Parser creation and detection
public static FeedParserBase CreateFeedParser(Feed feed, string feedText)
{
var feedType = DetectFeedType(feedText);
return feedType switch
{
FeedType.Rss => new RssParser(feed),
FeedType.Rdf => new RdfParser(feed),
FeedType.Atom => new AtomParser(feed),
_ => throw new ArgumentException($"Feed type {feedType} is not supported")
};
}
public static FeedType DetectFeedType(string feedText)
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Loop over all child nodes
foreach (XmlNode node in document.ChildNodes)
{
switch (node.Name)
{
case "rss":
return FeedType.Rss;
case "rdf:RDF":
return FeedType.Rdf;
case "feed":
return FeedType.Atom;
}
}
// No clue!
return FeedType.Unknown;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
}
#endregion
}
}
internal abstract class FeedParserBase
{
#region Member variables
protected readonly Feed Feed;
#endregion
#region Constructor
protected FeedParserBase(Feed feed)
{
Feed = feed;
}
#endregion
#region Methods
public abstract FeedReadResult ParseFeed(string feedText);
protected abstract FeedItem ParseFeedItem(XmlNode node);
protected void HandleFeedItem(XmlNode node, ref int sequence)
{
// Build a feed item from the node
var newFeedItem = ParseFeedItem(node);
if (newFeedItem == null)
return;
// Check for feed items with no guid or link
if (string.IsNullOrWhiteSpace(newFeedItem.Guid) && string.IsNullOrWhiteSpace(newFeedItem.Link))
return;
// Look for an item that has the same guid
var existingFeedItem = Feed.Items.FirstOrDefault(item => item.Guid == newFeedItem.Guid && item.Id != newFeedItem.Id);
// Check to see if we already have this feed item
if (existingFeedItem == null)
{
Log.Logger.Information("New link: " + newFeedItem.Link);
// Associate the new item with the right feed
newFeedItem.Feed = Feed;
// Set the item as new
newFeedItem.New = true;
// Add the item to the list
Feed.Items.Add(newFeedItem);
// Feed was updated
Feed.LastUpdated = DateTime.Now;
}
else
{
Log.Logger.Information("Existing link: " + newFeedItem.Link);
// Update the fields in the existing item
existingFeedItem.Link = newFeedItem.Link;
existingFeedItem.Title = newFeedItem.Title;
existingFeedItem.Guid = newFeedItem.Guid;
existingFeedItem.Description = newFeedItem.Description;
// Item is no longer new
existingFeedItem.New = false;
// Switch over to the existing item for the rest
newFeedItem = existingFeedItem;
}
// Item was last seen now
newFeedItem.LastFound = Feed.LastChecked;
// Set the sequence
newFeedItem.Sequence = sequence;
// Increment the sequence
sequence++;
}
#endregion
#region Parser creation and detection
public static FeedParserBase CreateFeedParser(Feed feed, string feedText)
{
var feedType = DetectFeedType(feedText);
return feedType switch
{
FeedType.Rss => new RssParser(feed),
FeedType.Rdf => new RdfParser(feed),
FeedType.Atom => new AtomParser(feed),
_ => throw new ArgumentException($"Feed type {feedType} is not supported")
};
}
public static FeedType DetectFeedType(string feedText)
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Loop over all child nodes
foreach (XmlNode node in document.ChildNodes)
{
switch (node.Name)
{
case "rss":
return FeedType.Rss;
case "rdf:RDF":
return FeedType.Rdf;
case "feed":
return FeedType.Atom;
}
}
// No clue!
return FeedType.Unknown;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
}
#endregion
}

View File

@@ -2,113 +2,112 @@
using Serilog;
using System.Xml;
namespace FeedCenter.FeedParsers
namespace FeedCenter.FeedParsers;
internal class RdfParser : FeedParserBase
{
internal class RdfParser : FeedParserBase
public RdfParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
{
public RdfParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
try
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Load the XML document from the text
document.LoadXml(feedText);
// Create the namespace manager
var namespaceManager = document.GetAllNamespaces();
// Create the namespace manager
var namespaceManager = document.GetAllNamespaces();
// Get the root node
XmlNode rootNode = document.DocumentElement;
// Get the root node
XmlNode rootNode = document.DocumentElement;
// If we didn't find a root node then bail
if (rootNode == null)
return FeedReadResult.UnknownError;
// If we didn't find a root node then bail
if (rootNode == null)
return FeedReadResult.UnknownError;
// Get the channel node
var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager);
if (channelNode == null)
return FeedReadResult.InvalidXml;
// Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "title":
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
Feed.Link = node.InnerText.Trim();
break;
case "description":
Feed.Description = node.InnerText;
break;
}
}
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the channel node
foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
// Get the channel node
var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager);
if (channelNode == null)
return FeedReadResult.InvalidXml;
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
// Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
switch (node.Name)
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
feedItem.Link = childNode.InnerText.Trim();
// RDF doesn't have a GUID node so we'll just use the link
feedItem.Guid = feedItem.Link;
Feed.Link = node.InnerText.Trim();
break;
case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
Feed.Description = node.InnerText;
break;
}
}
return feedItem;
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the channel node
foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml;
}
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "link":
feedItem.Link = childNode.InnerText.Trim();
// RDF doesn't have a GUID node so we'll just use the link
feedItem.Guid = feedItem.Link;
break;
case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break;
}
}
return feedItem;
}
}

View File

@@ -3,121 +3,120 @@ using Serilog;
using System;
using System.Xml;
namespace FeedCenter.FeedParsers
namespace FeedCenter.FeedParsers;
internal class RssParser : FeedParserBase
{
internal class RssParser : FeedParserBase
public RssParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
{
public RssParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
try
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Load the XML document from the text
document.LoadXml(feedText);
// Create the namespace manager
var namespaceManager = document.GetAllNamespaces();
// Create the namespace manager
var namespaceManager = document.GetAllNamespaces();
// Get the root node
XmlNode rootNode = document.DocumentElement;
// Get the root node
XmlNode rootNode = document.DocumentElement;
// If we didn't find a root node then bail
if (rootNode == null)
return FeedReadResult.UnknownError;
// If we didn't find a root node then bail
if (rootNode == null)
return FeedReadResult.UnknownError;
// Get the channel node
var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager) ??
rootNode.SelectSingleNode("channel", namespaceManager);
if (channelNode == null)
return FeedReadResult.InvalidXml;
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "title":
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
Feed.Link = node.InnerText.Trim();
break;
case "description":
Feed.Description = node.InnerText;
break;
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
// Get the channel node
var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager) ??
rootNode.SelectSingleNode("channel", namespaceManager);
if (channelNode == null)
return FeedReadResult.InvalidXml;
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
// Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
switch (node.Name)
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
feedItem.Link = childNode.InnerText.Trim();
break;
case "guid":
feedItem.Guid = childNode.InnerText.Trim();
var permaLink = true;
if (childNode.Attributes != null)
{
var permaLinkNode = childNode.Attributes.GetNamedItem("isPermaLink");
permaLink = permaLinkNode == null || permaLinkNode.Value == "true";
}
if (permaLink && Uri.IsWellFormedUriString(feedItem.Guid, UriKind.Absolute))
feedItem.Link = feedItem.Guid;
Feed.Link = node.InnerText.Trim();
break;
case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
Feed.Description = node.InnerText;
break;
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
if (string.IsNullOrWhiteSpace(feedItem.Guid))
feedItem.Guid = feedItem.Link;
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return feedItem;
return FeedReadResult.InvalidXml;
}
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "link":
feedItem.Link = childNode.InnerText.Trim();
break;
case "guid":
feedItem.Guid = childNode.InnerText.Trim();
var permaLink = true;
if (childNode.Attributes != null)
{
var permaLinkNode = childNode.Attributes.GetNamedItem("isPermaLink");
permaLink = permaLinkNode == null || permaLinkNode.Value == "true";
}
if (permaLink && Uri.IsWellFormedUriString(feedItem.Guid, UriKind.Absolute))
feedItem.Link = feedItem.Guid;
break;
case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break;
}
}
if (string.IsNullOrWhiteSpace(feedItem.Guid))
feedItem.Guid = feedItem.Link;
return feedItem;
}
}