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();
+ }
+}