| | | 1 | | using System.Net; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using BallSort.Engine.Models; |
| | | 4 | | using HtmlAgilityPack; |
| | | 5 | | |
| | | 6 | | namespace BallSort.Engine.Infrastructure; |
| | | 7 | | |
| | | 8 | | public sealed class PuzzleClient : IDisposable |
| | | 9 | | { |
| | | 10 | | private const string BaseUri = "https://puzzlemadness.co.uk/"; |
| | | 11 | | |
| | | 12 | | private readonly HttpClientHandler _handler; |
| | | 13 | | |
| | | 14 | | private readonly HttpClient _client; |
| | | 15 | | |
| | 0 | 16 | | private int _latestYear = 2005; |
| | | 17 | | |
| | 0 | 18 | | private readonly JsonSerializerOptions _jsonSerializerOptions = new() |
| | 0 | 19 | | { |
| | 0 | 20 | | PropertyNameCaseInsensitive = true |
| | 0 | 21 | | }; |
| | | 22 | | |
| | 0 | 23 | | public PuzzleClient() |
| | | 24 | | { |
| | 0 | 25 | | var cookieContainer = new CookieContainer(); |
| | | 26 | | |
| | 0 | 27 | | _handler = new HttpClientHandler |
| | 0 | 28 | | { |
| | 0 | 29 | | CookieContainer = cookieContainer, |
| | 0 | 30 | | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Bro |
| | 0 | 31 | | }; |
| | | 32 | | |
| | 0 | 33 | | var lines = File.ReadAllLines("cookies.txt"); |
| | | 34 | | |
| | 0 | 35 | | foreach (var line in lines) |
| | | 36 | | { |
| | 0 | 37 | | var parts = line.Split('='); |
| | | 38 | | |
| | 0 | 39 | | cookieContainer.Add(new Uri(BaseUri), new Cookie(parts[0], parts[1])); |
| | | 40 | | } |
| | | 41 | | |
| | 0 | 42 | | _client = new HttpClient(_handler) |
| | 0 | 43 | | { |
| | 0 | 44 | | BaseAddress = new Uri(BaseUri), |
| | 0 | 45 | | Timeout = TimeSpan.FromSeconds(30) |
| | 0 | 46 | | }; |
| | | 47 | | |
| | 0 | 48 | | _client.DefaultRequestHeaders.Add("User-Agent", |
| | 0 | 49 | | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/53 |
| | | 50 | | |
| | 0 | 51 | | _client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/ |
| | | 52 | | |
| | 0 | 53 | | _client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); |
| | | 54 | | |
| | 0 | 55 | | _client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.9"); |
| | 0 | 56 | | } |
| | | 57 | | |
| | | 58 | | public (DateOnly Date, Puzzle Puzzle)? GetPuzzle(Difficulty difficulty, DateOnly date) |
| | | 59 | | { |
| | 0 | 60 | | var year = date.Year; |
| | | 61 | | |
| | 0 | 62 | | var month = date.Month; |
| | | 63 | | |
| | 0 | 64 | | var day = date.Day; |
| | | 65 | | |
| | 0 | 66 | | using var response = _client.GetAsync($"ballsort/{difficulty}/{year}/{month}/{day}").Result; |
| | | 67 | | |
| | 0 | 68 | | var page = response.Content.ReadAsStringAsync().Result; |
| | | 69 | | |
| | 0 | 70 | | var puzzleJson = page[(page.IndexOf("puzzleData = ", StringComparison.InvariantCultureIgnoreCase) + 13)..]; |
| | | 71 | | |
| | 0 | 72 | | puzzleJson = puzzleJson[..puzzleJson.IndexOf(";", StringComparison.InvariantCultureIgnoreCase)]; |
| | | 73 | | |
| | 0 | 74 | | var puzzle = JsonSerializer.Deserialize<Puzzle>(puzzleJson, _jsonSerializerOptions); |
| | | 75 | | |
| | 0 | 76 | | return (date, puzzle); |
| | 0 | 77 | | } |
| | | 78 | | |
| | | 79 | | public (DateOnly Date, Puzzle Puzzle)? GetNextPuzzle(Difficulty difficulty) |
| | | 80 | | { |
| | 0 | 81 | | var nextPuzzleDate = GetOldestIncompletePuzzleDate(difficulty); |
| | | 82 | | |
| | 0 | 83 | | if (nextPuzzleDate == null) |
| | | 84 | | { |
| | 0 | 85 | | return null; |
| | | 86 | | } |
| | | 87 | | |
| | 0 | 88 | | Thread.Sleep(TimeSpan.FromMilliseconds(5_000)); |
| | | 89 | | |
| | 0 | 90 | | var year = nextPuzzleDate.Value.Year; |
| | | 91 | | |
| | 0 | 92 | | var month = nextPuzzleDate.Value.Month; |
| | | 93 | | |
| | 0 | 94 | | var day = nextPuzzleDate.Value.Day; |
| | | 95 | | |
| | 0 | 96 | | using var response = _client.GetAsync($"ballsort/{difficulty}/{year}/{month}/{day}").Result; |
| | | 97 | | |
| | 0 | 98 | | var page = response.Content.ReadAsStringAsync().Result; |
| | | 99 | | |
| | 0 | 100 | | var puzzleJson = page[(page.IndexOf("puzzleData = ", StringComparison.InvariantCultureIgnoreCase) + 13)..]; |
| | | 101 | | |
| | 0 | 102 | | puzzleJson = puzzleJson[..puzzleJson.IndexOf(";", StringComparison.InvariantCultureIgnoreCase)]; |
| | | 103 | | |
| | 0 | 104 | | var puzzle = JsonSerializer.Deserialize<Puzzle>(puzzleJson, _jsonSerializerOptions); |
| | | 105 | | |
| | 0 | 106 | | return (nextPuzzleDate.Value, puzzle); |
| | 0 | 107 | | } |
| | | 108 | | |
| | | 109 | | private DateOnly? GetOldestIncompletePuzzleDate(Difficulty difficulty) |
| | | 110 | | { |
| | 0 | 111 | | var now = DateTime.Now; |
| | | 112 | | |
| | 0 | 113 | | for (var year = _latestYear; year <= now.Year; year++) |
| | | 114 | | { |
| | 0 | 115 | | using var response = _client.GetAsync($"/archive/ballsort/{difficulty.ToString().ToLower()}/{year}").Result; |
| | | 116 | | |
| | 0 | 117 | | var page = response.Content.ReadAsStringAsync().Result; |
| | | 118 | | |
| | 0 | 119 | | var dom = new HtmlDocument(); |
| | | 120 | | |
| | 0 | 121 | | dom.LoadHtml(page); |
| | | 122 | | |
| | 0 | 123 | | var puzzles = dom.DocumentNode.SelectNodes("//td[@class='puzzleNotDone'] | //td[@class='puzzleSaveAvailable' |
| | | 124 | | |
| | 0 | 125 | | if (puzzles.Count > 0) |
| | | 126 | | { |
| | 0 | 127 | | var puzzle = puzzles[0]; |
| | | 128 | | |
| | 0 | 129 | | var id =puzzle.Attributes["id"].Value; |
| | | 130 | | |
| | 0 | 131 | | var parts = id.Split('-'); |
| | | 132 | | |
| | 0 | 133 | | _latestYear = year - 1; |
| | | 134 | | |
| | 0 | 135 | | return new DateOnly(int.Parse(parts[3]), int.Parse(parts[4]), int.Parse(parts[5])); |
| | | 136 | | } |
| | | 137 | | |
| | 0 | 138 | | Thread.Sleep(TimeSpan.FromMilliseconds(3_000)); |
| | | 139 | | } |
| | | 140 | | |
| | 0 | 141 | | return null; |
| | 0 | 142 | | } |
| | | 143 | | |
| | | 144 | | public void Dispose() |
| | | 145 | | { |
| | 0 | 146 | | _handler?.Dispose(); |
| | | 147 | | |
| | 0 | 148 | | _client?.Dispose(); |
| | 0 | 149 | | } |
| | | 150 | | } |