Files
viewer-count-fetcher/index.cs
T

282 lines
7.8 KiB
C#
Raw Normal View History

2025-10-28 16:10:44 -05:00
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
try
{
var settings = await LoadSettingsAsync();
var tokenResponse = await GetTokenResponseAsync(settings);
var viewerCount = await GetLiveViewerCountAsync(tokenResponse);
2025-10-29 09:58:02 -05:00
Console.Write(viewerCount);
2025-10-28 16:10:44 -05:00
}
catch (Exception ex)
{
2025-10-29 09:58:02 -05:00
Console.Write(ex.Message);
2025-10-28 16:10:44 -05:00
}
#region Methods
static async Task<int> GetLiveViewerCountAsync(TokenResponse tokenResponse)
{
using var client = new HttpClient();
var url = $"https://youtube.googleapis.com/youtube/v3/liveBroadcasts?part=statistics&broadcastStatus=active";
var requestUri = new Uri(url);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode is false)
{
throw new Exception("Failed to get broadcast");
}
var ytResponse = await response.Content.ReadFromJsonAsync(JsonContext.Default.YTResponse);
if (ytResponse is null || ytResponse.Items.Length is 0)
{
2025-10-29 09:51:51 -05:00
return -1;
2025-10-28 16:10:44 -05:00
}
var total = 0;
foreach (var item in ytResponse.Items)
{
total += item.Stats.Viewers;
}
return total;
2025-10-28 16:10:44 -05:00
}
static async Task<TokenResponse> GetTokenResponseAsync(Settings settings)
{
2025-11-19 09:45:51 -06:00
var tokenResponsePath = Path.Combine(GetEntryPointDirectory(), "tokenResponse.json");
2025-10-28 16:10:44 -05:00
if (File.Exists(tokenResponsePath) is false)
{
return await GetAccessTokenAsync(settings, tokenResponsePath);
}
else
{
var tokenResponseContent = await File.ReadAllTextAsync(tokenResponsePath);
var existingTokenResponse = JsonSerializer.Deserialize(tokenResponseContent, JsonContext.Default.TokenResponse) ??
throw new Exception("Failed to parse existing token response");
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (now >= existingTokenResponse.ExpiresAt)
{
return await RefreshTokenAsync(existingTokenResponse.RefreshToken, settings, tokenResponsePath);
}
return existingTokenResponse;
}
}
static async Task<Settings> LoadSettingsAsync()
{
2025-11-19 09:45:51 -06:00
var settingsPath = Path.Combine(GetEntryPointDirectory(), "appsettings.json");
2025-10-28 16:10:44 -05:00
var settingsContent = await File.ReadAllTextAsync(settingsPath);
var settings = JsonSerializer.Deserialize(settingsContent, JsonContext.Default.Settings);
if (settings is null)
{
throw new Exception("Failed to load settings");
}
return settings;
}
static async Task<TokenResponse> RefreshTokenAsync(string refreshToken, Settings settings, string tokenResponsePath)
{
using var client = new HttpClient();
var uri = new Uri(settings.TokenUri, UriKind.Absolute);
using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string?>
{
["client_id"] = settings.ClientId,
["client_secret"] = settings.ClientSecret,
["refresh_token"] = refreshToken,
["grant_type"] = "refresh_token"
})
};
var tokenResponse = await client.SendAsync(tokenRequest);
var tokenResponseContent = await tokenResponse.Content.ReadAsStringAsync();
if (tokenResponse.IsSuccessStatusCode is false)
{
throw new Exception("Failed to refresh OAuth token");
}
var newToken = JsonSerializer.Deserialize(tokenResponseContent, JsonContext.Default.TokenResponse) ??
throw new Exception("Failed to parse refreshed OAuth token");
var newTokenResponse = newToken.WithExpiresAt() with { RefreshToken = refreshToken };
var tokenResponseJson = JsonSerializer.Serialize(newTokenResponse, JsonContext.Default.TokenResponse);
await File.WriteAllTextAsync(tokenResponsePath, tokenResponseJson);
return newTokenResponse;
}
static async Task<TokenResponse> GetAccessTokenAsync(Settings settings, string tokenResponsePath)
{
var authUri = GetOAuthUri(settings);
using var process = Process.Start(new ProcessStartInfo
{
FileName = authUri,
UseShellExecute = true
});
using var listener = new HttpListener();
listener.Prefixes.Add(settings.RedirectUri);
listener.Start();
var listenerContext = await listener.GetContextAsync();
var oauthCode = listenerContext.Request.QueryString["code"];
try
{
var tokenResponse = await GetTokenAsync(oauthCode, settings);
var tokenResponseJson = JsonSerializer.Serialize(tokenResponse, JsonContext.Default.TokenResponse);
await File.WriteAllTextAsync(tokenResponsePath, tokenResponseJson);
return tokenResponse;
}
finally
{
var responseHtml = "<html><body><h1>You may now close this window.</h1></body></html>";
var buffer = Encoding.UTF8.GetBytes(responseHtml);
listenerContext.Response.ContentLength64 = buffer.Length;
await listenerContext.Response.OutputStream.WriteAsync(buffer);
listenerContext.Response.Close();
listener.Stop();
}
}
static string GetOAuthUri(Settings settings)
{
var authUriQueryParams = new Dictionary<string, string>
{
["client_id"] = settings.ClientId,
["redirect_uri"] = settings.RedirectUri,
["response_type"] = "code",
["scope"] = settings.Scopes,
["access_type"] = "offline",
["prompt"] = "consent",
};
var query = string.Join("&", authUriQueryParams.Select(static kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"));
var authUri = $"{settings.AuthUri}?{query}";
return authUri;
}
static async Task<TokenResponse> GetTokenAsync(string? code, Settings settings)
{
using var client = new HttpClient();
var uri = new Uri(settings.TokenUri, UriKind.Absolute);
using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string?>
{
["code"] = code,
["client_id"] = settings.ClientId,
["client_secret"] = settings.ClientSecret,
["redirect_uri"] = settings.RedirectUri,
["grant_type"] = "authorization_code"
})
};
var tokenResponse = await client.SendAsync(tokenRequest);
var tokenResponseContent = await tokenResponse.Content.ReadAsStringAsync();
if (tokenResponse.IsSuccessStatusCode is false)
{
throw new Exception("Failed to get OAuth token");
}
var token = JsonSerializer.Deserialize(tokenResponseContent, JsonContext.Default.TokenResponse) ?? throw new Exception("Failed to parse OAuth token");
return token.WithExpiresAt();
}
2025-11-19 09:45:51 -06:00
static string GetEntryPointDirectory()
{
return (string)AppContext.GetData("EntryPointFileDirectoryPath")!;
}
2025-10-28 16:10:44 -05:00
#endregion
#region Models
record Settings(
string ClientId,
string ClientSecret,
string RedirectUri,
string TokenUri,
string AuthUri,
string Scopes
);
record YTResponse(
[property: JsonPropertyName("items")]
YTItem[] Items
);
record YTItem(
[property: JsonPropertyName("statistics")]
YTStats Stats
);
record YTStats(
[property: JsonPropertyName("concurrentViewers")]
int Viewers
);
record TokenResponse(
[property: JsonPropertyName("access_token")]
string AccessToken,
[property: JsonPropertyName("expires_in")]
int ExpiresIn,
[property: JsonPropertyName("token_type")]
string TokenType,
[property: JsonPropertyName("scope")]
string Scope,
[property: JsonPropertyName("refresh_token")]
string RefreshToken,
[property: JsonPropertyName("expires_at")]
long ExpiresAt
)
{
public TokenResponse WithExpiresAt() => this with
{
ExpiresAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + (ExpiresIn * 1000)
};
}
[JsonSourceGenerationOptions(
WriteIndented = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
)]
[JsonSerializable(typeof(YTResponse))]
[JsonSerializable(typeof(YTItem))]
[JsonSerializable(typeof(YTStats))]
[JsonSerializable(typeof(Settings))]
[JsonSerializable(typeof(TokenResponse))]
internal sealed partial class JsonContext : JsonSerializerContext
{
}
#endregion