📄
BfiScreeningCheckerJob.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Playwright; using Quartz; [DisallowConcurrentExecution] internal sealed class BfiScreeningCheckerJob( PlaywrightBrowserService browserService, ScreeningRepository repository, IOptions<MonitorOptions> options, ILogger<BfiScreeningCheckerJob> logger ) : IJob { private static readonly Action<ILogger, int, Exception?> LogCheckComplete = LoggerMessage.Define<int>( LogLevel.Information, new EventId(1, nameof(LogCheckComplete)), "Check complete — {Count} available screening(s) across all pages" ); private static readonly Action<ILogger, string, string, Exception?> LogSmsScheduled = LoggerMessage.Define< string, string >( LogLevel.Information, new EventId(2, nameof(LogSmsScheduled)), "Scheduled SMS to {Phone} for screening {ScreeningId}" ); public async Task Execute(IJobExecutionContext context) { var opts = options.Value; var page = await browserService.NewPageAsync(); try { var available = new HashSet<string>(); await page.GotoAsync(opts.Url, new() { WaitUntil = WaitUntilState.DOMContentLoaded }); await page.WaitForSelectorAsync(".result-box-item"); while (true) { var availableItems = await page.QuerySelectorAllAsync(".result-box-item:has(.item-link.good)"); foreach (var item in availableItems) { var startDateEl = await item.QuerySelectorAsync(".start-date"); if (startDateEl is null) continue; var dateText = (await startDateEl.InnerTextAsync()).Trim(); if (!string.IsNullOrEmpty(dateText)) available.Add(dateText); } // Follow the next-page link if present. var nextLink = await page.QuerySelectorAsync("#av-next-link a"); if (nextLink is null) break; var nextHref = await nextLink.GetAttributeAsync("href"); if (string.IsNullOrEmpty(nextHref)) break; var nextUrl = new Uri(new Uri(page.Url), nextHref).ToString(); await page.GotoAsync(nextUrl, new() { WaitUntil = WaitUntilState.DOMContentLoaded }); await page.WaitForSelectorAsync(".result-box-item"); } LogCheckComplete(logger, available.Count, null); foreach (var screeningId in available) { if (await repository.HasBeenNotifiedAsync(screeningId)) continue; await repository.MarkNotifiedAsync(screeningId); var message = $"BFI IMAX tickets available! {screeningId} — Book now: {opts.Url}"; foreach (var phone in opts.PhoneNumbers) { var dataMap = new JobDataMap { { "phoneNumber", phone }, { "message", message } }; var job = JobBuilder .Create<SendSmsJob>() .WithIdentity(Guid.NewGuid().ToString(), "sms") .UsingJobData(dataMap) .Build(); var trigger = TriggerBuilder.Create().StartNow().Build(); await context.Scheduler.ScheduleJob(job, trigger, context.CancellationToken); LogSmsScheduled(logger, phone, screeningId, null); } } } finally { await page.CloseAsync(); } } }