1+ using System . Collections . Immutable ;
2+ using System . Net . Http . Headers ;
3+ using System . Text . Json ;
4+ using GrocyScanner . Core . Models ;
5+ using Microsoft . Extensions . Logging ;
6+
7+ namespace GrocyScanner . Core . Providers ;
8+
9+ public class MigrosProductProvider : IProductProvider
10+ {
11+ private readonly IHttpClientFactory _httpClientFactory ;
12+ private readonly ILogger < MigrosProductProvider > _logger ;
13+
14+ private string ? _authorizationHeader ;
15+
16+ public MigrosProductProvider ( IHttpClientFactory httpClientFactory , ILogger < MigrosProductProvider > logger )
17+ {
18+ _httpClientFactory = httpClientFactory ;
19+ _logger = logger ;
20+ }
21+
22+ public async Task < Product ? > GetProductByGtin ( string gtin )
23+ {
24+ IReadOnlyList < int > productIds = await GetProductIds ( gtin ) ;
25+
26+ if ( productIds . Count != 1 )
27+ {
28+ _logger . LogError ( "Unexpected count for gtin {Gtin}: {Count}" , gtin , productIds . Count ) ;
29+ return null ;
30+ }
31+
32+ return await GetProductByIdsAsync ( gtin , productIds ) ;
33+ }
34+
35+ private async Task < Product ? > GetProductByIdsAsync ( string gtin , IEnumerable < int > productIds )
36+ {
37+ using HttpClient httpClient = _httpClientFactory . CreateClient ( ) ;
38+
39+ HttpRequestMessage httpRequestMessage =
40+ new ( HttpMethod . Post , "https://www.migros.ch/product-display/public/v4/product-cards" ) ;
41+ string requestJson =
42+ @$ "{{""offerFilter"":{{""ongoingOfferDate"":""{ DateTime . Now : yyyy-MM-dd} T00:00:00"",""region"":""national"",""storeType"":""OFFLINE""}},""productFilter"":{{""uids"":[{ string . Join ( "," , productIds ) } ]}}}}";
43+ httpRequestMessage . Content = new StringContent (
44+ requestJson ,
45+ MediaTypeHeaderValue . Parse ( "application/json" ) ) ;
46+ httpRequestMessage . Headers . Add ( "leshopch" , await GetAndCreateAuthorizationHeader ( ) ) ;
47+ HttpResponseMessage httpResponseMessage = await httpClient . SendAsync ( httpRequestMessage ) ;
48+ string content = await httpResponseMessage . Content . ReadAsStringAsync ( ) ;
49+ try
50+ {
51+ httpResponseMessage . EnsureSuccessStatusCode ( ) ;
52+ using JsonDocument jsonDocument = JsonDocument . Parse ( content ) ;
53+
54+ if ( jsonDocument . RootElement . GetArrayLength ( ) != 1 )
55+ {
56+ _logger . LogError ( "Migros returned more than one product for the product ids {ProductId}" , productIds ) ;
57+ return null ;
58+ }
59+
60+ JsonElement . ArrayEnumerator arrayEnumerator = jsonDocument . RootElement . EnumerateArray ( ) ;
61+ arrayEnumerator . MoveNext ( ) ;
62+
63+ return new Product
64+ {
65+ Name = arrayEnumerator . Current . GetProperty ( "name" ) . GetString ( ) ! ,
66+ Gtin = gtin ,
67+ Categories = ImmutableList < string > . Empty ,
68+ ImageUrl = GetImageFromJsonDocument ( arrayEnumerator . Current )
69+ } ;
70+ }
71+ catch ( HttpRequestException httpRequestException )
72+ {
73+ _logger . LogError ( httpRequestException ,
74+ "Failed to request product details for {ProductIds} at Migros: {Content}" , productIds , content ) ;
75+ return null ;
76+ }
77+ }
78+
79+ private async ValueTask < string > GetAndCreateAuthorizationHeader ( )
80+ {
81+ if ( ! string . IsNullOrEmpty ( _authorizationHeader ) )
82+ {
83+ return _authorizationHeader ;
84+ }
85+
86+ using HttpClient httpClient = _httpClientFactory . CreateClient ( ) ;
87+ using HttpRequestMessage httpRequestMessage = new ( HttpMethod . Get , "https://www.migros.ch/authentication/public/v1/api/guest?authorizationNotRequired=true" ) ;
88+ HttpResponseMessage httpResponseMessage = await httpClient . SendAsync ( httpRequestMessage ) ;
89+ httpResponseMessage . EnsureSuccessStatusCode ( ) ;
90+ var values = httpResponseMessage . Headers . GetValues ( "leshopch" ) ;
91+ string authorizationToken = values . First ( ) ;
92+ _authorizationHeader = authorizationToken ;
93+ return authorizationToken ;
94+ }
95+
96+ private static string ? GetImageFromJsonDocument ( JsonElement jsonElement )
97+ {
98+ JsonElement imagesProperty = jsonElement . GetProperty ( "images" ) ;
99+ return imagesProperty
100+ . EnumerateArray ( )
101+ . Select ( element =>
102+ element . GetProperty ( "url" ) . GetString ( ) )
103+ . FirstOrDefault ( url => ! string . IsNullOrEmpty ( url ) ) ;
104+ }
105+
106+ private async Task < IReadOnlyList < int > > GetProductIds ( string gtin )
107+ {
108+ using HttpClient httpClient = _httpClientFactory . CreateClient ( ) ;
109+
110+ HttpRequestMessage httpRequestMessage =
111+ new ( HttpMethod . Post , "https://www.migros.ch/onesearch-oc-seaapi/public/v5/search" ) ;
112+ httpRequestMessage . Content = new StringContent (
113+ @$ "{{""algorithm"":""DEFAULT"",""filters"":{{}},""language"":""en"",""productIds"":[],""query"":{ gtin } ,""regionId"":""national"",""sortFields"":[],""sortOrder"":""asc""}}",
114+ MediaTypeHeaderValue . Parse ( "application/json" ) ) ;
115+ httpRequestMessage . Headers . Add ( "leshopch" , await GetAndCreateAuthorizationHeader ( ) ) ;
116+ HttpResponseMessage httpResponseMessage = await httpClient . SendAsync ( httpRequestMessage ) ;
117+ string content = await httpResponseMessage . Content . ReadAsStringAsync ( ) ;
118+ try
119+ {
120+ httpResponseMessage . EnsureSuccessStatusCode ( ) ;
121+ using JsonDocument jsonDocument = JsonDocument . Parse ( content ) ;
122+ JsonElement productIdsElement = jsonDocument . RootElement . GetProperty ( "productIds" ) ;
123+ return productIdsElement . EnumerateArray ( ) . Select ( jsonElement => jsonElement . GetInt32 ( ) ) . ToList ( ) ;
124+ }
125+ catch ( HttpRequestException httpRequestException )
126+ {
127+ _logger . LogError ( httpRequestException , "Failed to request {Gtin} at Migros: {Content}" , gtin , content ) ;
128+ return ImmutableList < int > . Empty ;
129+ }
130+ }
131+
132+ public string Name => "Migros" ;
133+ public string IconUri => "/migros-logo.png" ;
134+ public string Country => "Switzerland" ;
135+ }
0 commit comments