mirror of
https://github.com/alexhopeoconnor/bom-local-service.git
synced 2026-10-03 21:21:59 +10:00
Release v0.0.6: resilient BOM metadata parsing.
Read C07_WeatherMetadata via innerText, accept observation prefixes (a minute, N minutes, an hour, N hours) and forecast N hours ago. Version 0.0.6.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Version>0.0.5</Version>
|
<Version>0.0.6</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -41,26 +41,30 @@ public class TimeParsingService : ITimeParsingService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Look for the weather metadata section using the correct selector
|
// BOM layouts: legacy map used inner divs; location/spatial pages often use C07_WeatherMetadata with
|
||||||
// HTML structure: <section data-testid="weatherMetadata"> with aria-label="Last updated"
|
// a single section and visible text in the section (no div split). Prefer normalized innerText of the
|
||||||
var metadataSection = page.Locator("section[data-testid='weatherMetadata'], section[aria-label='Last updated']").First;
|
// section so both shapes parse the same.
|
||||||
|
var metadataSection = page.Locator(
|
||||||
// Extract text from each div separately to preserve structure
|
"section[data-testid='weatherMetadata'], " +
|
||||||
// Structure: <div>Observations: X minutes ago, 9:40 pm AEST</div><div>at Gympie...</div><div>Forecast: ...</div>
|
"section[aria-label='Last updated'], " +
|
||||||
|
"section[data-component='C07_WeatherMetadata']").First;
|
||||||
|
|
||||||
var lastUpdatedText = await page.EvaluateAsync<string>(@"() => {
|
var lastUpdatedText = await page.EvaluateAsync<string>(@"() => {
|
||||||
const section = document.querySelector('section[data-testid=""weatherMetadata""]') ||
|
const section =
|
||||||
document.querySelector('section[aria-label=""Last updated""]');
|
document.querySelector('section[data-testid=""weatherMetadata""]') ||
|
||||||
|
document.querySelector('section[aria-label=""Last updated""]') ||
|
||||||
|
document.querySelector('section[data-component=""C07_WeatherMetadata""]');
|
||||||
if (!section) return null;
|
if (!section) return null;
|
||||||
|
const raw = section.innerText || section.textContent || '';
|
||||||
// Get all divs and join with newlines to preserve structure
|
return raw.replace(/\s+/g, ' ').trim();
|
||||||
const divs = section.querySelectorAll('div');
|
|
||||||
return Array.from(divs).map(div => div.textContent.trim()).filter(text => text).join(' ');
|
|
||||||
}");
|
}");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(lastUpdatedText))
|
if (string.IsNullOrEmpty(lastUpdatedText))
|
||||||
{
|
{
|
||||||
// Fallback: get all text content
|
var fallback = await metadataSection.InnerTextAsync();
|
||||||
lastUpdatedText = await metadataSection.TextContentAsync();
|
lastUpdatedText = string.IsNullOrEmpty(fallback)
|
||||||
|
? null
|
||||||
|
: Regex.Replace(fallback, @"\s+", " ").Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseLastUpdatedText(lastUpdatedText ?? string.Empty);
|
return ParseLastUpdatedText(lastUpdatedText ?? string.Empty);
|
||||||
@@ -82,8 +86,10 @@ public class TimeParsingService : ITimeParsingService
|
|||||||
_logger.LogInformation("Parsing last updated text: {Text}", text);
|
_logger.LogInformation("Parsing last updated text: {Text}", text);
|
||||||
|
|
||||||
// Parse observation time from time string (e.g., "9:40 pm AEST")
|
// Parse observation time from time string (e.g., "9:40 pm AEST")
|
||||||
// Ignore "minutes ago" - client will calculate this dynamically from UTC timestamp
|
// Ignore relative prefix — BOM uses "a minute ago", "6 minutes ago", "an hour ago", "2 hours ago", etc.
|
||||||
var observationMatch = Regex.Match(text, @"Observations:\s*(?:\d+\s*minutes?\s*ago)?[,\s]+([\d:]+(?:\s*[ap]m)?)\s+([A-Z]{3,4})(?:at|\s+at|,|$)", RegexOptions.IgnoreCase);
|
var observationMatch = Regex.Match(text,
|
||||||
|
@"Observations:\s*(?:a\s+minute\s+ago|\d+\s*minutes?\s+ago|an\s+hour\s+ago|\d+\s*hours?\s+ago)?[,\s]+([\d:]+(?:\s*[ap]m)?)\s+([A-Z]{3,4})(?:at|\s+at|,|$)",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
if (observationMatch.Success && observationMatch.Groups.Count >= 3 && observationMatch.Groups[1].Success)
|
if (observationMatch.Success && observationMatch.Groups.Count >= 3 && observationMatch.Groups[1].Success)
|
||||||
{
|
{
|
||||||
var timeStr = observationMatch.Groups[1].Value.Trim();
|
var timeStr = observationMatch.Groups[1].Value.Trim();
|
||||||
@@ -114,8 +120,10 @@ public class TimeParsingService : ITimeParsingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse forecast time from time string (e.g., "7:50 pm AEST")
|
// Parse forecast time from time string (e.g., "7:50 pm AEST")
|
||||||
// Ignore "minutes ago" - client will calculate this dynamically from UTC timestamp
|
// Ignore relative prefix (minutes/hours ago) — client derives "ago" from the UTC timestamp
|
||||||
var forecastMatch = Regex.Match(text, @"Forecast:\s*(?:an\s+hour\s+ago|\d+\s*minutes?\s*ago)?[,\s]*([\d:]+(?:\s*[ap]m)?)\s+([A-Z]+)(?:\s+at|$)", RegexOptions.IgnoreCase);
|
var forecastMatch = Regex.Match(text,
|
||||||
|
@"Forecast:\s*(?:\d+\s*hours?\s+ago|an\s+hour\s+ago|\d+\s*minutes?\s*ago)?[,\s]*([\d:]+(?:\s*[ap]m)?)\s+([A-Z]+)(?:\s+at|$)",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
if (forecastMatch.Success && forecastMatch.Groups[1].Success)
|
if (forecastMatch.Success && forecastMatch.Groups[1].Success)
|
||||||
{
|
{
|
||||||
var timeStr = forecastMatch.Groups[1].Value.Trim();
|
var timeStr = forecastMatch.Groups[1].Value.Trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user