Skip to content

Commit

Permalink
feat: add implementation for coop product provider
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Jan 26, 2024
1 parent c7a70b6 commit d4a218d
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 2 deletions.
12 changes: 12 additions & 0 deletions GrocyScanner.Core/Extensions/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace GrocyScanner.Core.Extensions;

public static class DateTimeExtensions
{
public static long GetUnixMilliseconds(this DateTime date)
{
DateTime zero = new DateTime(1970, 1, 1);
TimeSpan span = date.Subtract(zero);

return (long)span.TotalMilliseconds;
}
}
12 changes: 12 additions & 0 deletions GrocyScanner.Core/Models/Coop/CoopProduct.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace GrocyScanner.Core.Models.Coop;

public class CoopProduct
{
[JsonPropertyName("success")]
public required bool Success { get; set; }

[JsonPropertyName("contentJsons")]
public required CoopProductContentJson ContentJsons { get; set; }
}
15 changes: 15 additions & 0 deletions GrocyScanner.Core/Models/Coop/CoopProductAnhor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace GrocyScanner.Core.Models.Coop;

public class CoopProductAnhor
{
[JsonPropertyName("anchor")]
public required string Anchor { get; set; }

[JsonPropertyName("name")]
public required string Name { get; set; }

[JsonPropertyName("json")]
public required CoopProductTile Json { get; set; }
}
9 changes: 9 additions & 0 deletions GrocyScanner.Core/Models/Coop/CoopProductContentJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace GrocyScanner.Core.Models.Coop;

public class CoopProductContentJson
{
[JsonPropertyName("anchors")]
public required List<CoopProductAnhor> Anchors { get; set; }
}
45 changes: 45 additions & 0 deletions GrocyScanner.Core/Models/Coop/CoopProductElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Text.Json.Serialization;

namespace GrocyScanner.Core.Models.Coop;

public class CoopProductElement
{
[JsonPropertyName("elementType")]
public required string ElementType { get; set; }

[JsonPropertyName("id")]
public required string Id { get; set; }

[JsonPropertyName("title")]
public required string Title { get; set; }

[JsonPropertyName("type")]
public required string Type { get; set; }

[JsonPropertyName("href")]
public required string Href { get; set; }

[JsonPropertyName("image")]
public required CoopProductImage Image { get; set; }

[JsonPropertyName("quantity")]
public required string Quantity { get; set; }

[JsonPropertyName("ratingAmount")]
public required int RatingAmount { get; set; }

[JsonPropertyName("ratingValue")]
public required double RatingValue { get; set; }

[JsonPropertyName("ratingLink")]
public required string RatingLink { get; set; }

[JsonPropertyName("brand")]
public string? Brand { get; set; }

[JsonPropertyName("price")]
public required string Price { get; set; }

[JsonPropertyName("udoCat")]
public required List<string> UdoCat { get; set; }
}
24 changes: 24 additions & 0 deletions GrocyScanner.Core/Models/Coop/CoopProductImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;

namespace GrocyScanner.Core.Models.Coop;

public class CoopProductImage
{
[JsonPropertyName("lazyload")]
public required bool Lazyload { get; set; }

[JsonPropertyName("lazyloadSrc")]
public required string LazyloadSrc { get; set; }

[JsonPropertyName("loader")]
public required string Loader { get; set; }

[JsonPropertyName("sizes")]
public required Dictionary<string, string> Sizes { get; set; }

[JsonPropertyName("src")]
public required string Src { get; set; }

[JsonPropertyName("srcset")]
public required List<List<string>> Srcset { get; set; }
}
12 changes: 12 additions & 0 deletions GrocyScanner.Core/Models/Coop/CoopProductTile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace GrocyScanner.Core.Models.Coop;

public class CoopProductTile
{
[JsonPropertyName("elements")]
public required List<CoopProductElement> Elements { get; set; }

[JsonPropertyName("context")]
public required string Context { get; set; }
}
82 changes: 80 additions & 2 deletions GrocyScanner.Core/Providers/CoopProductProvider.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,90 @@
using System.Text.Json;
using GrocyScanner.Core.Extensions;
using GrocyScanner.Core.Models;
using GrocyScanner.Core.Models.Coop;
using Microsoft.Extensions.Logging;

namespace GrocyScanner.Core.Providers;

public class CoopProductProvider : IProductProvider
{
public Task<Product?> GetProductByGtin(string gtin)
private const string DesiredAnchor = "[data-product-container='searchresults']";

private readonly IHttpClientFactory _httpClientFactory;

private readonly ILogger<CoopProductProvider> _logger;

public CoopProductProvider(IHttpClientFactory httpClientFactory, ILogger<CoopProductProvider> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}

public async Task<Product?> GetProductByGtin(string gtin)
{
long timeInMilliseconds = DateTime.Now.GetUnixMilliseconds();
using HttpClient httpClient = _httpClientFactory.CreateClient();

// do not pass any headers. Coop's API is really picky
HttpRequestMessage request = new(HttpMethod.Get,
$"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}");

HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);
string content = await httpResponseMessage.Content.ReadAsStringAsync();

try
{
httpResponseMessage.EnsureSuccessStatusCode();
CoopProduct coopProduct = JsonSerializer.Deserialize<CoopProduct>(content)!;

if (!coopProduct.Success)
{
_logger.LogError("Coop indicated that result was not successful: {Json}", content);
return null;
}

CoopProductAnhor? anchor = coopProduct.ContentJsons.Anchors.SingleOrDefault(anchor =>
anchor.Anchor.Equals(DesiredAnchor, StringComparison.Ordinal));
if (anchor == null)
{
_logger.LogError("Did not find the desired anchor for the product {Gtin}. Found {CountAnchor} anchors",
gtin, coopProduct.ContentJsons.Anchors.Count);
return null;
}

if (anchor.Json.Elements.Count != 1)
{
_logger.LogError("Search returned more than one item for the barcode: {CountProducts}",
anchor.Json.Elements.Count);
return null;
}

CoopProductElement element = anchor.Json.Elements.First();
return new Product
{
Name = element.Title,
Categories = element.UdoCat,
Gtin = gtin,
ImageUrl = GetBestQualityImage(element.Image)
};
}
catch (HttpRequestException httpRequestException)
{
_logger.LogError(httpRequestException, "Failed to fetch coop product: {Gtin} - {JsonResponse}", gtin,
content);
return null;
}
}

private static string GetBestQualityImage(CoopProductImage coopProductImage)
{
throw new NotImplementedException();
return coopProductImage.Srcset
.OrderByDescending(sourceSetPair =>
int.Parse(sourceSetPair
.Last()
.Trim('w')))
.First()
.First();
}

public string Name => "Coop";
Expand Down

0 comments on commit d4a218d

Please sign in to comment.