b53c7fb736
Features: - Category viewer ranges + subcategory templates (admin group modal, tree workspace) - Nomination enrichment via TwitchTracker API (NominationEnrichmentService, TwitchTrackerViewerStatsProvider) with admin tracking rules editor - Nomination group tracker: CategoryGroupName as primary identifier, CategoryId stays as nullable legacy field; StreamerIdentity table - Dynamic showact application form builder (AdminShowactFormBuilder, ShowactApplicationSchedule) - Session idle timeout setting (AdminSessionTimeoutCard) - Share URLs for X and Discord (SiteSettings, public extras) - Workflow rules now stored per season (falls back to global SiteSettings) - New admin routes: settings/access, settings/workflows, tracking-rules - New admin review workspace with subcategory tabs - AdminCategoriesView rebuilt with group/subcategory modals Migrations (all additive): - AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges, AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates, AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule, AddSeasonWorkflowRulesJson Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
2.8 KiB
C#
86 lines
2.8 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Backend.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Backend.Services;
|
|
|
|
public sealed class TwitchTrackerViewerStatsProvider(
|
|
IHttpClientFactory httpClientFactory,
|
|
AwardsDbContext db,
|
|
ILogger<TwitchTrackerViewerStatsProvider> logger)
|
|
: IViewerStatsProvider
|
|
{
|
|
public async Task<ViewerStatsSnapshot?> GetChannelSummaryAsync(string twitchLogin, CancellationToken cancellationToken)
|
|
{
|
|
var login = twitchLogin.Trim().TrimStart('@').ToLowerInvariant();
|
|
if (string.IsNullOrWhiteSpace(login))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
timeout.CancelAfter(TimeSpan.FromSeconds(3));
|
|
|
|
var settings = await db.SiteSettings
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(item => item.Id == 1, timeout.Token);
|
|
|
|
var baseUrl = TrackingRulesSettings.NormalizeBaseUrl(settings?.ViewerStatsProviderBaseUrl);
|
|
var client = httpClientFactory.CreateClient("TwitchTracker");
|
|
var response = await client.GetFromJsonAsync<TwitchTrackerChannelSummaryResponse>(
|
|
$"{baseUrl}/channels/summary/{Uri.EscapeDataString(login)}",
|
|
timeout.Token);
|
|
|
|
if (response is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new ViewerStatsSnapshot(
|
|
response.AverageViewers,
|
|
Math.Max(0, response.MinutesStreamed / 60),
|
|
response.HoursWatched,
|
|
response.PeakViewers,
|
|
response.FollowersGained,
|
|
TrackingRulesSettings.Window30d);
|
|
}
|
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
logger.LogInformation("TwitchTracker API lookup timed out for {TwitchLogin}.", login);
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogInformation(ex, "TwitchTracker API lookup failed for {TwitchLogin}.", login);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private sealed class TwitchTrackerChannelSummaryResponse
|
|
{
|
|
[JsonPropertyName("rank")]
|
|
public int Rank { get; init; }
|
|
|
|
[JsonPropertyName("minutes_streamed")]
|
|
public int MinutesStreamed { get; init; }
|
|
|
|
[JsonPropertyName("avg_viewers")]
|
|
public int AverageViewers { get; init; }
|
|
|
|
[JsonPropertyName("max_viewers")]
|
|
public int PeakViewers { get; init; }
|
|
|
|
[JsonPropertyName("hours_watched")]
|
|
public int HoursWatched { get; init; }
|
|
|
|
[JsonPropertyName("followers")]
|
|
public int FollowersGained { get; init; }
|
|
|
|
[JsonPropertyName("followers_total")]
|
|
public int FollowersTotal { get; init; }
|
|
}
|
|
}
|