Skip to content

Commit d4a218d

Browse files
committed
feat: add implementation for coop product provider
1 parent c7a70b6 commit d4a218d

File tree

8 files changed

+209
-2
lines changed

8 files changed

+209
-2
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace GrocyScanner.Core.Extensions;
2+
3+
public static class DateTimeExtensions
4+
{
5+
public static long GetUnixMilliseconds(this DateTime date)
6+
{
7+
DateTime zero = new DateTime(1970, 1, 1);
8+
TimeSpan span = date.Subtract(zero);
9+
10+
return (long)span.TotalMilliseconds;
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GrocyScanner.Core.Models.Coop;
4+
5+
public class CoopProduct
6+
{
7+
[JsonPropertyName("success")]
8+
public required bool Success { get; set; }
9+
10+
[JsonPropertyName("contentJsons")]
11+
public required CoopProductContentJson ContentJsons { get; set; }
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GrocyScanner.Core.Models.Coop;
4+
5+
public class CoopProductAnhor
6+
{
7+
[JsonPropertyName("anchor")]
8+
public required string Anchor { get; set; }
9+
10+
[JsonPropertyName("name")]
11+
public required string Name { get; set; }
12+
13+
[JsonPropertyName("json")]
14+
public required CoopProductTile Json { get; set; }
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GrocyScanner.Core.Models.Coop;
4+
5+
public class CoopProductContentJson
6+
{
7+
[JsonPropertyName("anchors")]
8+
public required List<CoopProductAnhor> Anchors { get; set; }
9+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GrocyScanner.Core.Models.Coop;
4+
5+
public class CoopProductElement
6+
{
7+
[JsonPropertyName("elementType")]
8+
public required string ElementType { get; set; }
9+
10+
[JsonPropertyName("id")]
11+
public required string Id { get; set; }
12+
13+
[JsonPropertyName("title")]
14+
public required string Title { get; set; }
15+
16+
[JsonPropertyName("type")]
17+
public required string Type { get; set; }
18+
19+
[JsonPropertyName("href")]
20+
public required string Href { get; set; }
21+
22+
[JsonPropertyName("image")]
23+
public required CoopProductImage Image { get; set; }
24+
25+
[JsonPropertyName("quantity")]
26+
public required string Quantity { get; set; }
27+
28+
[JsonPropertyName("ratingAmount")]
29+
public required int RatingAmount { get; set; }
30+
31+
[JsonPropertyName("ratingValue")]
32+
public required double RatingValue { get; set; }
33+
34+
[JsonPropertyName("ratingLink")]
35+
public required string RatingLink { get; set; }
36+
37+
[JsonPropertyName("brand")]
38+
public string? Brand { get; set; }
39+
40+
[JsonPropertyName("price")]
41+
public required string Price { get; set; }
42+
43+
[JsonPropertyName("udoCat")]
44+
public required List<string> UdoCat { get; set; }
45+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GrocyScanner.Core.Models.Coop;
4+
5+
public class CoopProductImage
6+
{
7+
[JsonPropertyName("lazyload")]
8+
public required bool Lazyload { get; set; }
9+
10+
[JsonPropertyName("lazyloadSrc")]
11+
public required string LazyloadSrc { get; set; }
12+
13+
[JsonPropertyName("loader")]
14+
public required string Loader { get; set; }
15+
16+
[JsonPropertyName("sizes")]
17+
public required Dictionary<string, string> Sizes { get; set; }
18+
19+
[JsonPropertyName("src")]
20+
public required string Src { get; set; }
21+
22+
[JsonPropertyName("srcset")]
23+
public required List<List<string>> Srcset { get; set; }
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace GrocyScanner.Core.Models.Coop;
4+
5+
public class CoopProductTile
6+
{
7+
[JsonPropertyName("elements")]
8+
public required List<CoopProductElement> Elements { get; set; }
9+
10+
[JsonPropertyName("context")]
11+
public required string Context { get; set; }
12+
}

GrocyScanner.Core/Providers/CoopProductProvider.cs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,90 @@
1+
using System.Text.Json;
2+
using GrocyScanner.Core.Extensions;
13
using GrocyScanner.Core.Models;
4+
using GrocyScanner.Core.Models.Coop;
5+
using Microsoft.Extensions.Logging;
26

37
namespace GrocyScanner.Core.Providers;
48

59
public class CoopProductProvider : IProductProvider
610
{
7-
public Task<Product?> GetProductByGtin(string gtin)
11+
private const string DesiredAnchor = "[data-product-container='searchresults']";
12+
13+
private readonly IHttpClientFactory _httpClientFactory;
14+
15+
private readonly ILogger<CoopProductProvider> _logger;
16+
17+
public CoopProductProvider(IHttpClientFactory httpClientFactory, ILogger<CoopProductProvider> logger)
18+
{
19+
_httpClientFactory = httpClientFactory;
20+
_logger = logger;
21+
}
22+
23+
public async Task<Product?> GetProductByGtin(string gtin)
24+
{
25+
long timeInMilliseconds = DateTime.Now.GetUnixMilliseconds();
26+
using HttpClient httpClient = _httpClientFactory.CreateClient();
27+
28+
// do not pass any headers. Coop's API is really picky
29+
HttpRequestMessage request = new(HttpMethod.Get,
30+
$"https://www.coop.ch/de/dynamic-pageload/searchresultJson?componentName=searchresultJson&url=%2Fde%2Fsearch%2F%3Ftext%3D{gtin}&displayUrl=%2Fde%2Fsearch%2F%3Ftext%3D{gtin}&compiledTemplates%5B%5D=productTile-new&compiledTemplates%5B%5D=sellingBanner&_={timeInMilliseconds}");
31+
32+
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);
33+
string content = await httpResponseMessage.Content.ReadAsStringAsync();
34+
35+
try
36+
{
37+
httpResponseMessage.EnsureSuccessStatusCode();
38+
CoopProduct coopProduct = JsonSerializer.Deserialize<CoopProduct>(content)!;
39+
40+
if (!coopProduct.Success)
41+
{
42+
_logger.LogError("Coop indicated that result was not successful: {Json}", content);
43+
return null;
44+
}
45+
46+
CoopProductAnhor? anchor = coopProduct.ContentJsons.Anchors.SingleOrDefault(anchor =>
47+
anchor.Anchor.Equals(DesiredAnchor, StringComparison.Ordinal));
48+
if (anchor == null)
49+
{
50+
_logger.LogError("Did not find the desired anchor for the product {Gtin}. Found {CountAnchor} anchors",
51+
gtin, coopProduct.ContentJsons.Anchors.Count);
52+
return null;
53+
}
54+
55+
if (anchor.Json.Elements.Count != 1)
56+
{
57+
_logger.LogError("Search returned more than one item for the barcode: {CountProducts}",
58+
anchor.Json.Elements.Count);
59+
return null;
60+
}
61+
62+
CoopProductElement element = anchor.Json.Elements.First();
63+
return new Product
64+
{
65+
Name = element.Title,
66+
Categories = element.UdoCat,
67+
Gtin = gtin,
68+
ImageUrl = GetBestQualityImage(element.Image)
69+
};
70+
}
71+
catch (HttpRequestException httpRequestException)
72+
{
73+
_logger.LogError(httpRequestException, "Failed to fetch coop product: {Gtin} - {JsonResponse}", gtin,
74+
content);
75+
return null;
76+
}
77+
}
78+
79+
private static string GetBestQualityImage(CoopProductImage coopProductImage)
880
{
9-
throw new NotImplementedException();
81+
return coopProductImage.Srcset
82+
.OrderByDescending(sourceSetPair =>
83+
int.Parse(sourceSetPair
84+
.Last()
85+
.Trim('w')))
86+
.First()
87+
.First();
1088
}
1189

1290
public string Name => "Coop";

0 commit comments

Comments
 (0)