Skip to content

Commit

Permalink
✨ Add simple service for parsing SUZ Canteena page
Browse files Browse the repository at this point in the history
  • Loading branch information
ostorc committed Oct 20, 2022
1 parent f3c9901 commit d7dc176
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 1 deletion.
1 change: 1 addition & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.5" />
<PackageReference Update="Shouldly" Version="4.1.0" />
<PackageReference Update="Html2Markdown" Version="5.1.0.703" />
<PackageReference Update="HtmlAgilityPack" Version="1.11.46" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "6.0.100",
"version": "6.0",
"rollForward": "latestMinor"
}
}
9 changes: 9 additions & 0 deletions src/HonzaBotner.Services.Contract/Dto/CanteenDishDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace HonzaBotner.Services.Contract.Dto;

public record CanteenDishDto(
string DishType,
string Name,
string Amount,
string StudentPrice,
string OtherPrice,
string PhotoLink = "");
5 changes: 5 additions & 0 deletions src/HonzaBotner.Services.Contract/Dto/CanteenDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Collections.Generic;

namespace HonzaBotner.Services.Contract.Dto;

public record CanteenDto(int Id, string Name, bool Open, IReadOnlyList<CanteenDishDto>? TodayDishes = null);
13 changes: 13 additions & 0 deletions src/HonzaBotner.Services.Contract/ICanteenService.cs
Original file line number Diff line number Diff line change
@@ -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<IList<CanteenDto>> ListCanteensAsync(bool onlyOpen = false, CancellationToken cancellationToken = default);
Task<CanteenDto> GetCurrentMenuAsync(CanteenDto canteen, CancellationToken cancellationToken = default);
}
54 changes: 54 additions & 0 deletions src/HonzaBotner.Services.Test/SuzCanteenServiceTest.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
1 change: 1 addition & 0 deletions src/HonzaBotner.Services/HonzaBotner.Services.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Html2Markdown" />
<PackageReference Include="HtmlAgilityPack" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HonzaBotner.Database\HonzaBotner.Database.csproj" />
Expand Down
114 changes: 114 additions & 0 deletions src/HonzaBotner.Services/SuzCanteenService.cs
Original file line number Diff line number Diff line change
@@ -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<IList<CanteenDto>> 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<CanteenDto> 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<CanteenDishDto>.Empty};
}

var dishes = new List<CanteenDishDto>();

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("&nbsp;", " ").Trim();
}
}

0 comments on commit d7dc176

Please sign in to comment.