< Summary

Information
Class: BallSort.Engine.Infrastructure.PuzzleClient
Assembly: BallSort.Engine
File(s): /home/runner/work/BallSort/BallSort/src/BallSort.Engine/Infrastructure/PuzzleClient.cs
Tag: 216
Line coverage
0%
Covered lines: 0
Uncovered lines: 70
Coverable lines: 70
Total lines: 150
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 12
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()0%620%
GetPuzzle(...)100%210%
GetNextPuzzle(...)0%620%
GetOldestIncompletePuzzleDate(...)0%2040%
Dispose()0%2040%

File(s)

/home/runner/work/BallSort/BallSort/src/BallSort.Engine/Infrastructure/PuzzleClient.cs

#LineLine coverage
 1using System.Net;
 2using System.Text.Json;
 3using BallSort.Engine.Models;
 4using HtmlAgilityPack;
 5
 6namespace BallSort.Engine.Infrastructure;
 7
 8public 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
 016    private int _latestYear = 2005;
 17
 018    private readonly JsonSerializerOptions _jsonSerializerOptions = new()
 019    {
 020        PropertyNameCaseInsensitive = true
 021    };
 22
 023    public PuzzleClient()
 24    {
 025        var cookieContainer = new CookieContainer();
 26
 027        _handler = new HttpClientHandler
 028        {
 029            CookieContainer = cookieContainer,
 030            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Bro
 031        };
 32
 033        var lines = File.ReadAllLines("cookies.txt");
 34
 035        foreach (var line in lines)
 36        {
 037            var parts = line.Split('=');
 38
 039            cookieContainer.Add(new Uri(BaseUri), new Cookie(parts[0], parts[1]));
 40        }
 41
 042        _client = new HttpClient(_handler)
 043        {
 044            BaseAddress = new Uri(BaseUri),
 045            Timeout = TimeSpan.FromSeconds(30)
 046        };
 47
 048        _client.DefaultRequestHeaders.Add("User-Agent",
 049            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/53
 50
 051        _client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/
 52
 053        _client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
 54
 055        _client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.9");
 056    }
 57
 58    public (DateOnly Date, Puzzle Puzzle)? GetPuzzle(Difficulty difficulty, DateOnly date)
 59    {
 060        var year = date.Year;
 61
 062        var month = date.Month;
 63
 064        var day = date.Day;
 65
 066        using var response = _client.GetAsync($"ballsort/{difficulty}/{year}/{month}/{day}").Result;
 67
 068        var page = response.Content.ReadAsStringAsync().Result;
 69
 070        var puzzleJson = page[(page.IndexOf("puzzleData = ", StringComparison.InvariantCultureIgnoreCase) + 13)..];
 71
 072        puzzleJson = puzzleJson[..puzzleJson.IndexOf(";", StringComparison.InvariantCultureIgnoreCase)];
 73
 074        var puzzle = JsonSerializer.Deserialize<Puzzle>(puzzleJson, _jsonSerializerOptions);
 75
 076        return (date, puzzle);
 077    }
 78
 79    public (DateOnly Date, Puzzle Puzzle)? GetNextPuzzle(Difficulty difficulty)
 80    {
 081        var nextPuzzleDate = GetOldestIncompletePuzzleDate(difficulty);
 82
 083        if (nextPuzzleDate == null)
 84        {
 085            return null;
 86        }
 87
 088        Thread.Sleep(TimeSpan.FromMilliseconds(5_000));
 89
 090        var year = nextPuzzleDate.Value.Year;
 91
 092        var month = nextPuzzleDate.Value.Month;
 93
 094        var day = nextPuzzleDate.Value.Day;
 95
 096        using var response = _client.GetAsync($"ballsort/{difficulty}/{year}/{month}/{day}").Result;
 97
 098        var page = response.Content.ReadAsStringAsync().Result;
 99
 0100        var puzzleJson = page[(page.IndexOf("puzzleData = ", StringComparison.InvariantCultureIgnoreCase) + 13)..];
 101
 0102        puzzleJson = puzzleJson[..puzzleJson.IndexOf(";", StringComparison.InvariantCultureIgnoreCase)];
 103
 0104        var puzzle = JsonSerializer.Deserialize<Puzzle>(puzzleJson, _jsonSerializerOptions);
 105
 0106        return (nextPuzzleDate.Value, puzzle);
 0107    }
 108
 109    private DateOnly? GetOldestIncompletePuzzleDate(Difficulty difficulty)
 110    {
 0111        var now = DateTime.Now;
 112
 0113        for (var year = _latestYear; year <= now.Year; year++)
 114        {
 0115            using var response = _client.GetAsync($"/archive/ballsort/{difficulty.ToString().ToLower()}/{year}").Result;
 116
 0117            var page = response.Content.ReadAsStringAsync().Result;
 118
 0119            var dom = new HtmlDocument();
 120
 0121            dom.LoadHtml(page);
 122
 0123            var puzzles = dom.DocumentNode.SelectNodes("//td[@class='puzzleNotDone'] | //td[@class='puzzleSaveAvailable'
 124
 0125            if (puzzles.Count > 0)
 126            {
 0127                var puzzle = puzzles[0];
 128
 0129                var id =puzzle.Attributes["id"].Value;
 130
 0131                var parts = id.Split('-');
 132
 0133                _latestYear = year - 1;
 134
 0135                return new DateOnly(int.Parse(parts[3]), int.Parse(parts[4]), int.Parse(parts[5]));
 136            }
 137
 0138            Thread.Sleep(TimeSpan.FromMilliseconds(3_000));
 139        }
 140
 0141        return null;
 0142    }
 143
 144    public void Dispose()
 145    {
 0146        _handler?.Dispose();
 147
 0148        _client?.Dispose();
 0149    }
 150}