src/monitors/CourseMonitor.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Monitors shooting-store.ch course catalog, fetching and parsing course data. .DESCRIPTION Inherits from MonitorBase. Implements web scraping for shooting-store.ch, extracting course details (name, date, time, price, availability) from HTML. Handles individual course detail page requests for availability information. .NOTES Provider: shooting-store (hardcoded) Parses HTML structure: artikel_box_content_wrapper, content artikel_box_name, artikel_preis Fetches availability from detail pages: data-update lagerinfo_anzeige Retries on failure up to RetryAttempts with TimeoutSeconds per request. #> class CourseMonitor : MonitorBase { CourseMonitor([object]$Config) : base($Config) { $this.Validate() } [object[]] Invoke() { $startTime = Get-Date Write-Log -Level INFO -Message "Monitor starting" -Context @{ id = $this.Id; provider = $this.Provider } try { $courses = $this._FetchAndParseCourses() if ($courses.Count -eq 0) { Write-Log -Level WARN -Message "No courses found" -Context @{ url = $this.Url } } else { Write-Log -Level INFO -Message "Courses fetched" -Context @{ count = $courses.Count; duration_ms = ((Get-Date) - $startTime).TotalMilliseconds } } return $courses } catch { Write-Log -Level ERROR -Message "Monitor execution failed" -Context @{ id = $this.Id } -Exception $_ throw } } hidden [object[]] _FetchAndParseCourses() { $response = Invoke-SecureWebRequest -Uri $this.Url -TimeoutSeconds $this.TimeoutSeconds $blocks = $response.Content -split '<div class="artikel_box_content_wrapper">' $courses = @() for ($i = 1; $i -lt $blocks.Count; $i++) { $block = $blocks[$i] if ($block -match '<a href="([^"]+)"[^>]*class="content artikel_box_name"[^>]*>([^<]+)</a>') { $detailHref = $Matches[1] $fullText = $Matches[2].Trim() $price = "" if ($block -match '<span class="artikel_preis\s*"[^>]*>\s*([^<]+)</span>') { $price = $Matches[1].Trim() } if ($fullText -match '^(.*?)\s+(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}-\d{2}:\d{2})$') { $courseName = $Matches[1].Trim() $courseDate = $Matches[2] $courseTime = $Matches[3] $availability = 0 if ($detailHref) { $detailUrl = "$($this.BaseUrl)$detailHref" if (-not (Test-ValidUrl -Url $detailUrl)) { Write-Log -Level WARN -Message "Invalid detail URL detected, skipping" -Context @{ url = $detailUrl } continue } try { $detailResponse = Invoke-SecureWebRequest -Uri $detailUrl -TimeoutSeconds $this.TimeoutSeconds if ($detailResponse.Content -match 'data-update="lagerinfo_anzeige\.lagerinfo">(\d+)\s+Artikel') { $availability = [int]$Matches[1] } } catch { Write-Log -Level WARN -Message "Failed to fetch availability details" -Context @{ url = $detailUrl } -Exception $_ } } $courses += @{ id = "$courseName|$courseDate|$courseTime" name = $courseName; date = $courseDate; time = $courseTime price = $price; availability = $availability url = "$($this.BaseUrl)$detailHref" monitor_id = $this.Id fetched_at = [datetime]::UtcNow.ToString('o') } } } } return $courses } } |