include cache update

This commit is contained in:
fox
2026-06-24 11:51:15 +01:00
parent 689276ea7d
commit a2a6c15808
64 changed files with 744 additions and 1104 deletions
@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Quartz;
namespace Solicitors.CacheBuild;
@@ -10,6 +12,43 @@ public static class DIExtensions
{
return services
.AddScoped<ISolicitorParser, SolicitorsDotCom.SolicitorParser>()
.AddScoped<ISolicitorImporter, SolicitorImporter>();
.AddScoped<ISolicitorImporter, SolicitorImporter>()
.AddQuartz()
.AddQuartzHostedService(opt => opt.WaitForJobsToComplete = true);
}
public static async Task UseCacheBuildAsync(this IServiceScope scope, CancellationToken cancellationToken)
{
var factory = scope.ServiceProvider.GetRequiredService<ISchedulerFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<ImportConfiguration>>().Value;
var scheduler = await factory.GetScheduler(cancellationToken);
var importJob = JobBuilder.Create<ImportRunner>()
.WithIdentity("ImportJob", "ImportGroup")
.Build();
var importTrigger = TriggerBuilder.Create()
.WithIdentity("ImportRunner", "ImportGroup")
.StartNow()
.WithSimpleSchedule(builder => builder
.WithIntervalInMinutes(options.ImportMinutes)
.RepeatForever())
.Build();
await scheduler.ScheduleJob(importJob, importTrigger, cancellationToken);
var staleJob = JobBuilder.Create<StaleDataRemover>()
.WithIdentity("StaleDataRemover", "RemoveGroup")
.Build();
var staleTrigger = TriggerBuilder.Create()
.WithIdentity("StaleDataTrigger", "RemoveGroup")
.StartNow()
.WithSimpleSchedule(builder => builder
.WithIntervalInMinutes(options.RemoveMinutes)
.RepeatForever())
.Build();
await scheduler.ScheduleJob(staleJob, staleTrigger, cancellationToken);
}
}
@@ -0,0 +1,8 @@
namespace Solicitors.CacheBuild;
public class ImportConfiguration
{
public int StaleMinutes { get; set; }
public int RemoveMinutes { get; set; }
public int ImportMinutes { get; set; }
}
@@ -0,0 +1,15 @@
using Quartz;
using Solicitors.Core.Data;
namespace Solicitors.CacheBuild;
internal class ImportRunner(
ISolicitorImporter importer,
ISolicitorRepository repository) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
await repository.EnsureCreatedAsync(context.CancellationToken);
await importer.RunFullImport(context.CancellationToken);
}
}
@@ -3,27 +3,18 @@ using Solicitors.Core.Models.Imports;
namespace Solicitors.CacheBuild;
internal class SolicitorImporter : ISolicitorImporter
internal class SolicitorImporter(
ISolicitorParser parser,
ISolicitorRepository repository) : ISolicitorImporter
{
private readonly ISolicitorParser _parser;
private readonly ISolicitorRepository _repository;
public SolicitorImporter(
ISolicitorParser parser,
ISolicitorRepository repository)
{
_parser = parser;
_repository = repository;
}
public async Task RunFullImport(CancellationToken cancellationToken)
{
foreach (var solicitor in await _parser.GetSolicitorsAsync(cancellationToken))
foreach (var solicitor in await parser.GetSolicitorsAsync(cancellationToken))
{
await RunImport(solicitor, cancellationToken);
}
}
private Task RunImport(SolicitorData solicitor, CancellationToken cancellationToken)
=> _repository.AddOrUpdateSolicitorAsync(solicitor, cancellationToken);
=> repository.AddOrUpdateSolicitorAsync(solicitor, cancellationToken);
}
@@ -13,6 +13,15 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<PackageReference Include="Quartz" Version="3.18.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.18.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
<HintPath>..\..\..\..\..\..\..\.dotnet\shared\Microsoft.AspNetCore.App\10.0.7\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using Solicitors.Core.Misc;
using Solicitors.Core.Models.Imports;
using Solicitors.HtmlParsing;
@@ -154,7 +153,7 @@ internal class SolicitorParser : ISolicitorParser
ParseSidebar(sidebarChildren, out phone, out email, out website, out ratings);
}
return new SolicitorData()
return new SolicitorData
{
Name = builder.Name,
UrlPath = builder.Path,
@@ -168,14 +167,14 @@ internal class SolicitorParser : ISolicitorParser
};
}
Location[] ParseOffices(IEnumerable<IHtmlNode> officeNodes)
private Location[] ParseOffices(IEnumerable<IHtmlNode> officeNodes)
=> officeNodes
.Select(ParseOffice)
.Where(x => x is not null)
.Cast<Location>()
.ToArray();
Location? ParseOffice(IHtmlNode officeNode)
private Location? ParseOffice(IHtmlNode officeNode)
{
if (!officeNode.TryGetChildren(out var officeChildren)
|| !officeNode.TryGetByTagName("address", out var addressNode)
@@ -183,7 +182,7 @@ internal class SolicitorParser : ISolicitorParser
return null;
Location? location = null;
string? address = "";
var address = "";
foreach (var line in addressLines)
{
if (line.TryGetText(out var lineText))
@@ -214,7 +213,7 @@ internal class SolicitorParser : ISolicitorParser
return location;
}
void ParseSidebar(
private void ParseSidebar(
IEnumerable<IHtmlNode> sidebarChildren,
out string? phone,
out string? email,
@@ -253,7 +252,7 @@ internal class SolicitorParser : ISolicitorParser
foreach (var child in childrenArray.Where(x => x.TryGetByClass("rev-box", out _, true)))
{
if (child.TryGetAttributeValue("title", out var ratingString)
&& TryParseRatingString(ratingString, out decimal rating, out decimal maxRating)
&& TryParseRatingString(ratingString, out var rating, out var maxRating)
&& child.TryGetByTagNameAndClass("img", "rev-logo", out var imgNode)
&& imgNode.TryGetAttributeValue("src", out var imgSrc)
&& TryParseRatingSrc(imgSrc, out var ratingProvider))
@@ -290,15 +289,15 @@ internal class SolicitorParser : ISolicitorParser
return decimal.TryParse(parts[0], out rating) && decimal.TryParse(parts[1], out maxRating);
}
bool TryParseRatingSrc(string imgSrc, out string provider)
private bool TryParseRatingSrc(string imgSrc, out string provider)
{
provider = "";
imgSrc = imgSrc.Trim('"');
var prefix = "/images/logo-";
const string prefix = "/images/logo-";
if (!imgSrc.StartsWith(prefix))
return false;
var providerAndExtension = imgSrc.Substring(prefix.Length);
var providerAndExtension = imgSrc[prefix.Length..];
provider = providerAndExtension.Split('.').First();
return true;
}
@@ -306,11 +305,11 @@ internal class SolicitorParser : ISolicitorParser
bool TryParseOwnRatingString(string ratingString, out decimal rating)
{
rating = 0;
var prefix = "Average review score : ";
const string prefix = "Average review score : ";
if (!ratingString.StartsWith(prefix))
return false;
var ratingPart = ratingString.Substring(prefix.Length);
var ratingPart = ratingString[prefix.Length..];
return decimal.TryParse(ratingPart, out rating);
}
}
@@ -0,0 +1,19 @@
using Microsoft.Extensions.Options;
using Quartz;
using Solicitors.Core.Data;
namespace Solicitors.CacheBuild;
internal class StaleDataRemover(
IOptions<ImportConfiguration> config,
ISolicitorRepository repository) : IJob
{
private readonly ImportConfiguration _config = config.Value;
public Task Execute(IJobExecutionContext context)
{
return repository.RemoveStaleEntriesAsync(
TimeSpan.FromMinutes(_config.StaleMinutes),
context.CancellationToken);
}
}