Skip to content

Commit a86edd1

Browse files
author
Andrii Bondarchuk
committed
Add local function to retrieve riders maxVelo for zwift event
1 parent 650a9ce commit a86edd1

File tree

11 files changed

+327
-19
lines changed

11 files changed

+327
-19
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using FitSyncHub.Zwift.HttpClients;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Azure.Functions.Worker;
5+
6+
namespace FitSyncHub.Functions.Functions;
7+
8+
public class ZwiftEventVELORatingHttpTriggerFunction
9+
{
10+
private readonly ZwiftEventsService _zwiftEventsService;
11+
private readonly ZwiftRacingHttpClient _zwiftRacingHttpClient;
12+
13+
public ZwiftEventVELORatingHttpTriggerFunction(
14+
ZwiftEventsService zwiftEventsService,
15+
ZwiftRacingHttpClient zwiftRacingHttpClient)
16+
{
17+
_zwiftEventsService = zwiftEventsService;
18+
_zwiftRacingHttpClient = zwiftRacingHttpClient;
19+
}
20+
21+
#if DEBUG
22+
[Function(nameof(ZwiftEventVELORatingHttpTriggerFunction))]
23+
#endif
24+
public async Task<IActionResult> Run(
25+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "zwift-event-vELO-rating")] HttpRequest req,
26+
CancellationToken cancellationToken)
27+
{
28+
string? cookie = req.Query["cookie"];
29+
string? zwiftEventUrl = req.Query["eventUrl"];
30+
string? subcategory = req.Query["subcategory"];
31+
32+
if (string.IsNullOrWhiteSpace(cookie)
33+
|| string.IsNullOrWhiteSpace(zwiftEventUrl)
34+
|| string.IsNullOrWhiteSpace(subcategory))
35+
{
36+
return new BadRequestObjectResult("wrong request");
37+
}
38+
39+
if (!Uri.TryCreate(zwiftEventUrl, UriKind.Absolute, out _))
40+
{
41+
return new BadRequestObjectResult("wrong url");
42+
}
43+
44+
45+
var entrants = await _zwiftEventsService.GetEntrants(zwiftEventUrl, subcategory, cancellationToken);
46+
47+
var year = DateTime.UtcNow.Year;
48+
List<ZwiftEventVELORatingResponseItem> items = [];
49+
50+
foreach (var rider in entrants)
51+
{
52+
var history = await _zwiftRacingHttpClient.GetRiderHistory(cookie, rider.Id, year: year, cancellationToken: cancellationToken);
53+
54+
var maxVelo = history.History.Max(x => x.Rating);
55+
var minVelo = history.History.Min(x => x.Rating);
56+
var velo = history.History
57+
.OrderByDescending(x => x.UpdatedAt)
58+
.FirstOrDefault()?.Rating;
59+
var ftpPerKg = rider.Ftp / (rider.Weight / 1000.0);
60+
61+
items.Add(new ZwiftEventVELORatingResponseItem
62+
{
63+
Id = rider.Id,
64+
FirstName = rider.FirstName,
65+
LastName = rider.LastName,
66+
Age = rider.Age,
67+
Weight = rider.Weight / 1000.0,
68+
FtpPerKg = ftpPerKg,
69+
MaxVELO = maxVelo,
70+
MinVELO = minVelo,
71+
VELO = velo,
72+
});
73+
}
74+
75+
var result = new ZwiftEventVELORatingResponse
76+
{
77+
Year = DateTime.UtcNow.Year,
78+
Items = [.. items.OrderByDescending(x => x.MaxVELO)],
79+
};
80+
81+
return new OkObjectResult(result);
82+
}
83+
}
84+
85+
86+
public record ZwiftEventVELORatingResponse
87+
{
88+
public required int Year { get; init; }
89+
public required ZwiftEventVELORatingResponseItem[] Items { get; init; }
90+
public int Count => Items.Length;
91+
92+
}
93+
94+
public record ZwiftEventVELORatingResponseItem
95+
{
96+
public required int Id { get; init; }
97+
public required string FirstName { get; init; }
98+
public required string LastName { get; init; }
99+
public required int Age { get; init; }
100+
public required double Weight { get; init; }
101+
public required double? MaxVELO { get; init; }
102+
public required double? MinVELO { get; init; }
103+
public required double? VELO { get; init; }
104+
public required double FtpPerKg { get; init; }
105+
}

src/FitSyncHub.Zwift/HttpClients/ZwiftAuthDelegatingHandler.cs renamed to src/FitSyncHub.Zwift/HttpClients/DelegatingHandlers/ZwiftAuthDelegatingHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Microsoft.Extensions.DependencyInjection;
44
using Polly;
55

6-
namespace FitSyncHub.Zwift.HttpClients;
6+
namespace FitSyncHub.Zwift.HttpClients.DelegatingHandlers;
77

88
public class ZwiftAuthDelegatingHandler : DelegatingHandler
99
{
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace FitSyncHub.Zwift.HttpClients.Models.Responses.Events;
2+
3+
public record ZwiftEventSubgroupEntrantResponse
4+
{
5+
public required int Id { get; init; }
6+
public required string PublicId { get; init; }
7+
public required string FirstName { get; init; }
8+
public required string LastName { get; init; }
9+
public required bool Male { get; init; }
10+
public required string EventCategory { get; init; }
11+
public required int Age { get; init; }
12+
public required int BodyType { get; init; }
13+
public required int Height { get; init; } // in mm
14+
public required int Weight { get; init; } // in grams
15+
public required int Ftp { get; init; }
16+
public required int AchievementLevel { get; init; }
17+
public required long TotalDistance { get; init; } // meters
18+
public required long TotalDistanceClimbed { get; init; } // meters
19+
public required int TotalTimeInMinutes { get; init; }
20+
public required int TotalInKomJersey { get; init; }
21+
public required int TotalInSprintersJersey { get; init; }
22+
public required int TotalInOrangeJersey { get; init; }
23+
public required int TotalWattHours { get; init; }
24+
public required int TotalExperiencePoints { get; init; }
25+
public required int TargetExperiencePoints { get; init; }
26+
public required long TotalGold { get; init; }
27+
public required int StreaksCurrentLength { get; init; }
28+
public required double TotalWattHoursPerKg { get; init; }
29+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
namespace FitSyncHub.Zwift.HttpClients.Models.Responses.ZwiftRacing;
2+
3+
public record ZwiftRacingRiderResponse
4+
{
5+
public required int RiderId { get; init; }
6+
public required bool Male { get; init; }
7+
public required ZwiftRacingPowerData Power { get; init; }
8+
public required List<ZwiftRacingHistoryEntry> History { get; init; }
9+
}
10+
11+
public record ZwiftRacingPowerData
12+
{
13+
public List<double>? Wkg5 { get; init; }
14+
public List<double>? Wkg15 { get; init; }
15+
public List<double>? Wkg30 { get; init; }
16+
public List<double>? Wkg60 { get; init; }
17+
public List<double>? Wkg120 { get; init; }
18+
public List<double>? Wkg300 { get; init; }
19+
public List<double>? Wkg1200 { get; init; }
20+
}
21+
22+
public record ZwiftRacingHistoryEntry
23+
{
24+
public string? Id { get; init; }
25+
public ZwiftRacingEventData? Event { get; init; }
26+
public int? RiderId { get; init; }
27+
public string? Source { get; init; }
28+
public string? Name { get; init; }
29+
public string? TeamId { get; init; }
30+
public string? TeamName { get; init; }
31+
public string? Country { get; init; }
32+
public double? Weight { get; init; }
33+
public int? Height { get; init; }
34+
public string? Age { get; init; }
35+
public bool? Male { get; init; }
36+
public string? Category { get; init; }
37+
public double? Time { get; init; }
38+
public double? TimeGun { get; init; }
39+
public double? Gap { get; init; }
40+
public int? Position { get; init; }
41+
public int? PositionInCategory { get; init; }
42+
public double? Points { get; init; } // nullable
43+
public double? AvgSpeed { get; init; }
44+
public double? WkgAvg { get; init; }
45+
public double? Wkg5 { get; init; }
46+
public double? Wkg15 { get; init; }
47+
public double? Wkg30 { get; init; }
48+
public double? Wkg60 { get; init; }
49+
public double? Wkg120 { get; init; }
50+
public double? Wkg300 { get; init; }
51+
public double? Wkg1200 { get; init; }
52+
public int? Np { get; init; }
53+
public int? Ftp { get; init; }
54+
public string? ZpCat { get; init; }
55+
public double? Load { get; init; }
56+
public List<object>? RatingScenarios { get; init; }
57+
public double? Distance { get; init; }
58+
public ZwiftRacingHeartRate? HeartRate { get; init; }
59+
public DateTime? CreatedAt { get; init; }
60+
public DateTime? UpdatedAt { get; init; }
61+
public double? Rating { get; init; }
62+
public double? RatingBefore { get; init; }
63+
public int? PenTotal { get; init; }
64+
public int? PenTimeCut { get; init; }
65+
}
66+
67+
public record ZwiftRacingEventData
68+
{
69+
public string? Id { get; init; }
70+
public long? Time { get; init; }
71+
public string? Title { get; init; }
72+
public string? Type { get; init; }
73+
public string? SubType { get; init; }
74+
public double? Distance { get; init; }
75+
public double? Elevation { get; init; }
76+
public ZwiftRacingRouteData? Route { get; init; }
77+
}
78+
79+
public record ZwiftRacingRouteData
80+
{
81+
public string? RouteId { get; init; }
82+
public string? World { get; init; }
83+
public string? Name { get; init; }
84+
public string? Profile { get; init; }
85+
}
86+
87+
public record ZwiftRacingHeartRate
88+
{
89+
public int? Avg { get; init; }
90+
public int? Max { get; init; }
91+
}

src/FitSyncHub.Zwift/HttpClients/ZwiftUnauthorizedHttpClient.cs renamed to src/FitSyncHub.Zwift/HttpClients/ZwiftHttpClientUnauthorized.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
namespace FitSyncHub.Zwift.HttpClients;
44

5-
public class ZwiftUnauthorizedHttpClient
5+
public class ZwiftHttpClientUnauthorized
66
{
77
private readonly HttpClient _httpClient;
88

9-
public ZwiftUnauthorizedHttpClient(HttpClient httpClient)
9+
public ZwiftHttpClientUnauthorized(HttpClient httpClient)
1010
{
1111
_httpClient = httpClient;
1212
}

src/FitSyncHub.Zwift/HttpClients/ZwiftHttpClient_Events.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,21 @@ namespace FitSyncHub.Zwift.HttpClients;
66

77
public partial class ZwiftHttpClient
88
{
9-
public async Task<string> GetRaceResultsForSubgroup(int eventSubgroupId)
9+
public async Task<ZwiftEventResponse> GetEvent(string eventUrl, CancellationToken cancellationToken)
10+
{
11+
var url = eventUrl.Replace(
12+
"https://www.zwift.com/uk/events/view/",
13+
"/api/public/events/");
14+
15+
var response = await _httpClient.GetAsync(url, cancellationToken);
16+
response.EnsureSuccessStatusCode();
17+
18+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
19+
20+
return JsonSerializer.Deserialize(content, ZwiftEventsGenerationContext.Default.ZwiftEventResponse)!;
21+
}
22+
23+
public async Task<string> GetEventSubgroupResults(int eventSubgroupId, CancellationToken cancellationToken)
1024
{
1125
List<JsonElement> entriesResult = [];
1226
const long TakeCount = 50;
@@ -15,10 +29,10 @@ public async Task<string> GetRaceResultsForSubgroup(int eventSubgroupId)
1529
{
1630
var url = $"/api/race-results/entries?event_subgroup_id={eventSubgroupId}&limit={TakeCount}&start={entriesResult.Count}";
1731

18-
var response = await _httpClient.GetAsync(url);
32+
var response = await _httpClient.GetAsync(url, cancellationToken);
1933
response.EnsureSuccessStatusCode();
2034

21-
var content = await response.Content.ReadAsStringAsync();
35+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
2236
var jsonDocument = JsonDocument.Parse(content);
2337
var entriesProperty = jsonDocument.RootElement.GetProperty("entries");
2438
var entriesDocuments = entriesProperty.EnumerateArray();
@@ -30,17 +44,24 @@ public async Task<string> GetRaceResultsForSubgroup(int eventSubgroupId)
3044
return JsonSerializer.Serialize(mergedJson, ZwiftEventsGenerationContext.Default.Options);
3145
}
3246

33-
public async Task<ZwiftEventResponse> GetEvent(string zwiftEventUrl)
47+
public async Task<IReadOnlyCollection<ZwiftEventSubgroupEntrantResponse>> GetEventSubgroupEntrants(
48+
int eventSubgroupId,
49+
string type = "all",
50+
string participation = "signed_up",
51+
CancellationToken cancellationToken = default)
3452
{
35-
var url = zwiftEventUrl.Replace(
36-
"https://www.zwift.com/uk/events/view/",
37-
"/api/public/events/");
53+
// see sauce source code how to handle pagination
54+
const long TakeCount = 100;
55+
const long Start = 0;
56+
57+
var url = $"/api/events/subgroups/entrants/{eventSubgroupId}?type={type}&participation={participation}&limit={TakeCount}&start={Start}";
3858

39-
var response = await _httpClient.GetAsync(url);
59+
var response = await _httpClient.GetAsync(url, cancellationToken);
4060
response.EnsureSuccessStatusCode();
4161

42-
var content = await response.Content.ReadAsStringAsync();
62+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
4363

44-
return JsonSerializer.Deserialize(content, ZwiftEventsGenerationContext.Default.ZwiftEventResponse)!;
64+
return JsonSerializer.Deserialize(content,
65+
ZwiftEventsGenerationContext.Default.IReadOnlyCollectionZwiftEventSubgroupEntrantResponse)!;
4566
}
4667
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text.Json;
2+
using FitSyncHub.Zwift.HttpClients.Models.Responses.ZwiftRacing;
3+
using FitSyncHub.Zwift.JsonSerializerContexts;
4+
5+
namespace FitSyncHub.Zwift.HttpClients;
6+
7+
public class ZwiftRacingHttpClient
8+
{
9+
private readonly HttpClient _httpClient;
10+
11+
public ZwiftRacingHttpClient(HttpClient httpClient)
12+
{
13+
_httpClient = httpClient;
14+
}
15+
16+
public async Task<ZwiftRacingRiderResponse> GetRiderHistory(
17+
// it was awful solution with delegating handler to add cookie(cause scoped service resolves two times), so i just pass cookie here
18+
string cookie,
19+
long riderId,
20+
int? year = default,
21+
CancellationToken cancellationToken = default)
22+
{
23+
var url = $"/api/riders/{riderId}/history?year={year ?? DateTime.Now.Year}";
24+
25+
var request = new HttpRequestMessage(HttpMethod.Get, url);
26+
request.Headers.Add("Cookie", cookie);
27+
28+
var response = await _httpClient.SendAsync(request, cancellationToken);
29+
response.EnsureSuccessStatusCode();
30+
31+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
32+
33+
return JsonSerializer.Deserialize(content,
34+
ZwiftRacingGenerationContext.Default.ZwiftRacingRiderResponse)!;
35+
}
36+
}

src/FitSyncHub.Zwift/JsonSerializerContexts/ZwiftEventsGenerationContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ namespace FitSyncHub.Zwift.JsonSerializerContexts;
99
)]
1010
[JsonSerializable(typeof(IReadOnlyCollection<ZwiftEventResponse>))]
1111
[JsonSerializable(typeof(IReadOnlyCollection<ZwiftRaceResultResponse>))]
12+
[JsonSerializable(typeof(IReadOnlyCollection<ZwiftEventSubgroupEntrantResponse>))]
1213
internal partial class ZwiftEventsGenerationContext : JsonSerializerContext;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Text.Json.Serialization;
2+
using FitSyncHub.Zwift.HttpClients.Models.Responses.ZwiftRacing;
3+
4+
namespace FitSyncHub.Zwift.JsonSerializerContexts;
5+
6+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
7+
[JsonSerializable(typeof(ZwiftRacingRiderResponse))]
8+
internal partial class ZwiftRacingGenerationContext : JsonSerializerContext;

0 commit comments

Comments
 (0)