Skip to content

Commit

Permalink
Merge pull request #14 from echaritonidis/feature/feed-content-image
Browse files Browse the repository at this point in the history
Feature/feed content image
  • Loading branch information
echaritonidis authored Apr 21, 2024
2 parents a1a6323 + c04e325 commit 08945b2
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 129 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,6 @@ healthchecksdb

# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/

# Ignore .idea folder
/.idea
11 changes: 8 additions & 3 deletions Client/Pages/Feed/FeedContentView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
foreach (var feedContent in Model)
{
<a class="feed-content" href="@feedContent.Link" target="_blank">
<h5>@feedContent.Title</h5>
<p class="published"><i>Published on @feedContent.PubDate</i></p>
<p class="desc">@((MarkupString)feedContent.Description)</p>
<div class="feed-content-linear">
<img alt="link preview" src="data:image/jpeg;base64,@feedContent.ImageBase64" />
<div>
<h5>@feedContent.Title</h5>
<p class="published"><i>Published on @feedContent.PubDate</i></p>
<p class="desc">@((MarkupString)feedContent.Description.Substring(0, 100))</p>
</div>
</div>
</a>
}
}
34 changes: 18 additions & 16 deletions Client/wwwroot/css/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,41 +91,43 @@
min-height: 700px;
grid-area: mainArea;
}

#main .main-content .loader {
position: relative;
top: 34px;
}

#main .main-content a {
#main .main-content .loader {
position: relative;
top: 34px;
}
#main .main-content .feed-content .feed-content-linear {
display: grid;
grid-template-columns: 130px auto;
}
#main .main-content .feed-content {
margin-bottom: 20px;
display: block;
padding: 6px 10px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
#main .main-content a:hover {
background-color: #f2f2f2;
#main .main-content .feed-content:nth-of-type(odd) {
background-color: #f2f2f2;
}
#main .main-content .feed-content:hover h5 {
text-decoration: underline;
}
#main .main-content a:hover h5 {
text-decoration: underline;
}
#main .main-content h5 {
#main .main-content .feed-content h5 {
font-size: 1.15rem;
}

#main .main-content .published {
#main .main-content .feed-content .published {
font-size: 0.9rem;
margin: 0 0 5px 0;
color: #898989;
}
#main .main-content .desc {
#main .main-content .feed-content .desc {
font-size: 0.95rem;
margin: 0;
overflow: hidden;
}
#main .main-content .desc img {
#main .main-content .feed-content .desc img {
max-width: 100%;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ public async Task<Guid> InsertAsync(TEntity obj, CancellationToken cancellationT
await _dataContext.SaveChangesAsync(cancellationToken);

return obj.Id;
}

public async Task<bool> InsertManyAsync(List<TEntity> objs, CancellationToken cancellationToken)
{
objs.ForEach(obj => _dataContext.Entry(obj).State = EntityState.Added);

}

public async Task<bool> InsertManyAsync(List<TEntity> objs, CancellationToken cancellationToken)
{
objs.ForEach(obj => _dataContext.Entry(obj).State = EntityState.Added);

await _dataContext.SaveChangesAsync(cancellationToken);

return true;
}

}

public async Task UpdateAsync(TEntity obj, CancellationToken cancellationToken)
{
_dataContext.Entry(obj).State = EntityState.Modified;
Expand Down
20 changes: 10 additions & 10 deletions Server/Infrastructure/Services/Contracts/IExtractContent.cs
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using RssFeeder.Shared.Model;

namespace RssFeeder.Server.Infrastructure.Services.Contracts
{
public interface IExtractContent
{
public List<FeedContent> GetContentItems(string xmlContent);
}
}
using RssFeeder.Shared.Model;

namespace RssFeeder.Server.Infrastructure.Services.Contracts
{
public interface IExtractContent
{
public Task<List<FeedContent>> GetContentItems(string xmlContent);
}
}

8 changes: 8 additions & 0 deletions Server/Infrastructure/Services/Contracts/IExtractImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace RssFeeder.Server.Infrastructure.Services.Contracts
{
public interface IExtractImage
{
public Task<string> GetImageBase64ByHref(string href);
}
}

123 changes: 63 additions & 60 deletions Server/Infrastructure/Services/Implementations/ExtractContent.cs
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,60 +1,63 @@
using System.Xml.Linq;
using RssFeeder.Server.Infrastructure.Services.Contracts;
using RssFeeder.Server.Infrastructure.Utils;
using RssFeeder.Shared.Extensions;
using RssFeeder.Shared.Model;

namespace RssFeeder.Server.Infrastructure.Services.Implementations
{
public class ExtractContent : IExtractContent
{
private readonly DateRegexUtil _dateRegexUtil;

public ExtractContent(DateRegexUtil dateRegexUtil)
{
_dateRegexUtil = dateRegexUtil;
}

public List<FeedContent> GetContentItems(string xmlContent)
{
List<FeedContent> result = new();

var xml = XDocument.Parse(xmlContent);

var channel = xml.Descendants("channel");
var items = channel.Descendants("item");

foreach (var item in items)
{
var link = item.GetElement("link");

if (string.IsNullOrEmpty(link))
{
link = item.GetElement("guid");
}

var pubDate = item.GetElement("pubDate");
var match = _dateRegexUtil.IsMatch(pubDate);

if (match.Success)
{
pubDate = match.Value;
}

result.Add
(
new FeedContent
{
Title = item.GetElement("title"),
Link = link,
Description = item.GetElement("description"),
PubDate = pubDate
}
);
}

return result;
}
}
}

using System.Xml.Linq;
using RssFeeder.Server.Infrastructure.Services.Contracts;
using RssFeeder.Server.Infrastructure.Utils;
using RssFeeder.Shared.Extensions;
using RssFeeder.Shared.Model;

namespace RssFeeder.Server.Infrastructure.Services.Implementations
{
public class ExtractContent : IExtractContent
{
private readonly IExtractImage _extractImage;
private readonly DateRegexUtil _dateRegexUtil;

public ExtractContent(IExtractImage extractImage, DateRegexUtil dateRegexUtil)
{
_extractImage = extractImage;
_dateRegexUtil = dateRegexUtil;
}

public async Task<List<FeedContent>> GetContentItems(string xmlContent)
{
List<FeedContent> result = new();

var xml = XDocument.Parse(xmlContent);

var channel = xml.Descendants("channel");
var items = channel.Descendants("item");

foreach (var item in items)
{
var link = item.GetElement("link");

if (string.IsNullOrEmpty(link))
{
link = item.GetElement("guid");
}

var pubDate = item.GetElement("pubDate");
var match = _dateRegexUtil.IsMatch(pubDate);

if (match.Success)
{
pubDate = match.Value;
}

result.Add
(
new FeedContent
{
Title = item.GetElement("title"),
Link = link,
ImageBase64 = await _extractImage.GetImageBase64ByHref(link),
Description = item.GetElement("description"),
PubDate = pubDate
}
);
}

return result;
}
}
}

104 changes: 104 additions & 0 deletions Server/Infrastructure/Services/Implementations/ExtractImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using RssFeeder.Server.Infrastructure.Services.Contracts;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;

namespace RssFeeder.Server.Infrastructure.Services.Implementations
{
public class ExtractImage : IExtractImage
{
private readonly IHttpClientFactory _httpClientFactory;
private const int MAX_ALLOWED_WIDTH = 120;

public ExtractImage(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}

public async Task<string> GetImageBase64ByHref(string href)
{
var httpClient = _httpClientFactory.CreateClient();

HttpResponseMessage response = await httpClient.GetAsync(href);
string html = await response.Content.ReadAsStringAsync();

var parser = new HtmlParser();
var document = parser.ParseDocument(html);

if (document is null || document.Head is null) return string.Empty;

var previewImageUrl = GetPreviewImageUrl(document);

if (!string.IsNullOrEmpty(previewImageUrl))
{
// Download the preview image and resize it
using (var imageStream = await httpClient.GetStreamAsync(previewImageUrl))
{
return await ResizeImage(imageStream);
}
}

return string.Empty;
}

private string GetPreviewImageUrl(IHtmlDocument document)
{
// Try to extract the preview image URL using the 'og:image' meta tag
var ogImageNode = document!.Head!.QuerySelector("meta[property='og:image']");
string previewImageUrl = ogImageNode?.GetAttribute("content");

// If the 'og:image' meta tag is not present, try the 'twitter:image' meta tag
if (previewImageUrl == null)
{
var twitterImageNode = document.Head.QuerySelector("meta[property='twitter:image']");
previewImageUrl = twitterImageNode?.GetAttribute("content");
}

// If the 'twitter:image' meta tag is not present, try the 'link[rel='image_src']' tag
if (previewImageUrl == null)
{
var imageSrcNode = document.Head.QuerySelector("link[rel='image_src']");
previewImageUrl = imageSrcNode?.GetAttribute("href");
}

// If none of the above methods work, try selecting the first 'img' tag with a 'src' attribute
if (previewImageUrl == null)
{
var firstImageNode = document.QuerySelector("img[src]");
previewImageUrl = firstImageNode?.GetAttribute("src");
}

return previewImageUrl;
}

private async Task<string> ResizeImage(Stream imageStream)
{
using (var image = await Image.LoadAsync(imageStream))
{
int maxWidth = MAX_ALLOWED_WIDTH;

if (image.Width > maxWidth)
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxWidth, 0),
Mode = ResizeMode.Max
}));
}

// Save the resized image to a file or stream
// For example, you can save it to a file like this:
// image.Save("preview.jpg", new JpegEncoder());

// Convert the resized image to a Base64 string
var memoryStream = new MemoryStream();
image.Save(memoryStream, new JpegEncoder());
var base64String = Convert.ToBase64String(memoryStream.ToArray());

return base64String;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,6 @@ public async Task<OneOf<List<FeedContent>, NotFound, CustomHttpRequestException,
return new NotFound();
}

return _extractContent.GetContentItems(xmlContent);
return await _extractContent.GetContentItems(xmlContent);
}
}
Loading

0 comments on commit 08945b2

Please sign in to comment.