diff --git a/Packages.props b/Packages.props index aab9af91..ea79371c 100644 --- a/Packages.props +++ b/Packages.props @@ -24,5 +24,6 @@ + diff --git a/global.json b/global.json index 1b8195c4..899e6b3a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.100", + "version": "6.0", "rollForward": "latestMinor" } } diff --git a/src/HonzaBotner.Services.Contract/Dto/CanteenDishDto.cs b/src/HonzaBotner.Services.Contract/Dto/CanteenDishDto.cs new file mode 100644 index 00000000..cad66dd4 --- /dev/null +++ b/src/HonzaBotner.Services.Contract/Dto/CanteenDishDto.cs @@ -0,0 +1,9 @@ +namespace HonzaBotner.Services.Contract.Dto; + +public record CanteenDishDto( + string DishType, + string Name, + string Amount, + string StudentPrice, + string OtherPrice, + string PhotoLink = ""); diff --git a/src/HonzaBotner.Services.Contract/Dto/CanteenDto.cs b/src/HonzaBotner.Services.Contract/Dto/CanteenDto.cs new file mode 100644 index 00000000..09bb1054 --- /dev/null +++ b/src/HonzaBotner.Services.Contract/Dto/CanteenDto.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace HonzaBotner.Services.Contract.Dto; + +public record CanteenDto(int Id, string Name, bool Open, IReadOnlyList? TodayDishes = null); diff --git a/src/HonzaBotner.Services.Contract/ICanteenService.cs b/src/HonzaBotner.Services.Contract/ICanteenService.cs new file mode 100644 index 00000000..b51a27ee --- /dev/null +++ b/src/HonzaBotner.Services.Contract/ICanteenService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using HonzaBotner.Services.Contract.Dto; + +namespace HonzaBotner.Services.Contract; + +public interface ICanteenService +{ + Task> ListCanteensAsync(bool onlyOpen = false, CancellationToken cancellationToken = default); + Task GetCurrentMenuAsync(CanteenDto canteen, CancellationToken cancellationToken = default); +} diff --git a/src/HonzaBotner.Services.Test/SuzCanteenServiceTest.cs b/src/HonzaBotner.Services.Test/SuzCanteenServiceTest.cs new file mode 100644 index 00000000..766a4874 --- /dev/null +++ b/src/HonzaBotner.Services.Test/SuzCanteenServiceTest.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Net.Http; +using HonzaBotner.Services.Contract; +using HonzaBotner.Services.Contract.Dto; +using Xunit; + +namespace HonzaBotner.Services.Test; + +public class SuzCanteenServiceTest +{ + private static readonly CanteenDto Technicka = new(3, "Technická menza", true); + + [Fact] + public async void ListCanteens() + { + ICanteenService canteenService = new SuzCanteenService(new HttpClient()); // TODO: Init + + var canteens = (await canteenService.ListCanteensAsync()).ToList(); + + Assert.NotEmpty(canteens); + Assert.All(canteens, dto => + { + Assert.NotEmpty(dto.Name); + Assert.NotEqual(0, dto.Id); + }); + + Assert.Contains(Technicka, canteens); + } + + [Fact] + public async void GetCurrentMenu() + { + ICanteenService canteenService = new SuzCanteenService(new HttpClient()); // TODO: Init + var canteens = await canteenService.ListCanteensAsync(true); + + foreach (CanteenDto canteen in canteens) + { + var canteenWithMenu = await canteenService.GetCurrentMenuAsync(canteen); + // Method modifies only today dishes + Assert.Equal(canteen, canteenWithMenu with { TodayDishes = canteen.TodayDishes }); + Assert.NotNull(canteenWithMenu); + + foreach (var dish in canteenWithMenu.TodayDishes!) + { + Assert.NotEmpty(dish.DishType); + Assert.NotEqual("Jiné", dish.DishType); + Assert.NotEmpty(dish.Name); + Assert.NotEmpty(dish.StudentPrice); + Assert.NotEmpty(dish.OtherPrice); + } + } + } +} diff --git a/src/HonzaBotner.Services/HonzaBotner.Services.csproj b/src/HonzaBotner.Services/HonzaBotner.Services.csproj index fbc867a8..98965744 100644 --- a/src/HonzaBotner.Services/HonzaBotner.Services.csproj +++ b/src/HonzaBotner.Services/HonzaBotner.Services.csproj @@ -6,6 +6,7 @@ + diff --git a/src/HonzaBotner.Services/SuzCanteenService.cs b/src/HonzaBotner.Services/SuzCanteenService.cs new file mode 100644 index 00000000..1e6e3baa --- /dev/null +++ b/src/HonzaBotner.Services/SuzCanteenService.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using HonzaBotner.Services.Contract; +using HonzaBotner.Services.Contract.Dto; +using HtmlAgilityPack; + +namespace HonzaBotner.Services; + +public class SuzCanteenService: ICanteenService +{ + private readonly HttpClient _httpClient; + + public SuzCanteenService(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task> ListCanteensAsync(bool onlyOpen = false, CancellationToken cancellationToken = default) + { + const string url = "https://agata.suz.cvut.cz/jidelnicky/index.php"; + string pageContent = await _httpClient.GetStringAsync(url, cancellationToken); + + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(pageContent); + + var canteens = htmlDoc.DocumentNode.SelectNodes("//ul[@id='menzy']/li/a") + .Select(node => + { + int.TryParse(node.Id.Replace("podSh", ""), out int id); + bool open = node.SelectSingleNode($"{node.XPath}/img") + .GetAttributeValue("src", "closed").Contains("Otevreno"); + string name = node.InnerText.Trim(); + return new CanteenDto(id, name, open); + }); + + if (onlyOpen) + return canteens.Where(c => c.Open).ToList(); + return canteens.ToList(); + } + + public async Task GetCurrentMenuAsync(CanteenDto canteen, CancellationToken cancellationToken = default) + { + const string url = "https://agata.suz.cvut.cz/jidelnicky/index.php?clPodsystem={0}"; + string pageContent = await _httpClient.GetStringAsync(string.Format(url, canteen.Id), cancellationToken); + + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(pageContent); + + var rows = htmlDoc.DocumentNode + .SelectNodes("//div[@class='data']/table[@class='table table-condensed']/tbody/tr"); + if (rows is null) + { + // TODO: Log and maybe say something to user? + return canteen with { TodayDishes = ArraySegment.Empty}; + } + + var dishes = new List(); + + string currentDishType = "Jiné"; + + foreach (var row in rows) + { + if (row is null) continue; + + var th = row.ChildNodes["th"]; + if (th is not null) + { + currentDishType = th.InnerText.Trim(); + continue; + } + + var tds = row.SelectNodes("td"); + if (tds is null) continue; + + string amount = TrimHtml(tds[1].InnerText); + string name = TrimHtml(tds[2].InnerText); + string photo = ParsePhoto(tds[4]); + + string studentPrice = TrimHtml(tds[5].InnerText); + string otherPrice = TrimHtml(tds[6].InnerText); + + dishes.Add(new CanteenDishDto(currentDishType, name, amount, studentPrice, otherPrice, photo)); + } + + + return canteen with { TodayDishes = dishes }; + } + + + private static string ParsePhoto(HtmlNode node) + { + var photo = node.Descendants("a")?.FirstOrDefault(); + + if (photo is null) + { + return string.Empty; + } + + string link = photo.GetAttributeValue("href", string.Empty); + + if (string.IsNullOrEmpty(link)) return link; + + return $"https://agata.suz.cvut.cz/jidelnicky/{link}"; + } + + private static string TrimHtml(string text) + { + return text.Replace(" ", " ").Trim(); + } +}