YouTubeApiConnectionTester.cs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. using System.Text.Json;
  2. using Application.Abstractions.YouTube;
  3. using Microsoft.Extensions.Logging;
  4. namespace Infrastructure.YouTube;
  5. internal sealed class YouTubeApiConnectionTester(
  6. IHttpClientFactory httpClientFactory,
  7. ILogger<YouTubeApiConnectionTester> logger
  8. ) : IYouTubeApiConnectionTester
  9. {
  10. public async Task<YouTubeApiTestResult> TestAsync(string testChannelId, CancellationToken ct)
  11. {
  12. // "YouTubeApi" 명명 HttpClient = YouTubeApiKeyHandler가 자동으로 ?key= 붙여줌
  13. var client = httpClientFactory.CreateClient("YouTubeApi");
  14. var url = $"https://www.googleapis.com/youtube/v3/channels?part=snippet&id={Uri.EscapeDataString(testChannelId)}";
  15. try
  16. {
  17. using var response = await client.GetAsync(url, ct);
  18. var body = await response.Content.ReadAsStringAsync(ct);
  19. var status = (int)response.StatusCode;
  20. if (!response.IsSuccessStatusCode)
  21. {
  22. var msg = ExtractGoogleErrorMessage(body) ?? Truncate(body, 300);
  23. logger.LogWarning("[YouTube] API key test failed: {Status} — {Body}", status, Truncate(body, 500));
  24. return new YouTubeApiTestResult(
  25. Ok: false,
  26. HttpStatus: status,
  27. ChannelTitle: null,
  28. ChannelId: testChannelId,
  29. ErrorMessage: $"HTTP {status} {response.StatusCode} — {msg}"
  30. );
  31. }
  32. try
  33. {
  34. var doc = JsonSerializer.Deserialize<JsonElement>(body);
  35. if (!doc.TryGetProperty("items", out var items) || items.GetArrayLength() == 0)
  36. {
  37. return new YouTubeApiTestResult(
  38. Ok: false,
  39. HttpStatus: status,
  40. ChannelTitle: null,
  41. ChannelId: testChannelId,
  42. ErrorMessage: "응답의 items가 비어있습니다. API Key는 유효하나 채널 조회가 제한됐을 수 있음 (Cloud Console에서 API Key 제한 설정 확인)."
  43. );
  44. }
  45. var title = items[0].GetProperty("snippet").GetProperty("title").GetString();
  46. return new YouTubeApiTestResult(
  47. Ok: true,
  48. HttpStatus: status,
  49. ChannelTitle: title,
  50. ChannelId: testChannelId,
  51. ErrorMessage: null
  52. );
  53. }
  54. catch (JsonException ex)
  55. {
  56. return new YouTubeApiTestResult(
  57. Ok: false,
  58. HttpStatus: status,
  59. ChannelTitle: null,
  60. ChannelId: testChannelId,
  61. ErrorMessage: $"응답 JSON 파싱 실패: {ex.Message}"
  62. );
  63. }
  64. }
  65. catch (Exception ex)
  66. {
  67. logger.LogError(ex, "[YouTube] API key test threw");
  68. return new YouTubeApiTestResult(
  69. Ok: false,
  70. HttpStatus: null,
  71. ChannelTitle: null,
  72. ChannelId: testChannelId,
  73. ErrorMessage: $"요청 실패: {ex.Message}"
  74. );
  75. }
  76. }
  77. // Google API 에러 응답: { "error": { "code": 403, "message": "...", "errors": [{reason:"keyInvalid"}] } }
  78. private static string? ExtractGoogleErrorMessage(string body)
  79. {
  80. if (string.IsNullOrWhiteSpace(body))
  81. {
  82. return null;
  83. }
  84. try
  85. {
  86. var doc = JsonSerializer.Deserialize<JsonElement>(body);
  87. if (doc.TryGetProperty("error", out var err))
  88. {
  89. var msg = err.TryGetProperty("message", out var m) ? m.GetString() : null;
  90. var reason = err.TryGetProperty("errors", out var errs) && errs.GetArrayLength() > 0
  91. && errs[0].TryGetProperty("reason", out var r) ? r.GetString() : null;
  92. if (!string.IsNullOrEmpty(msg))
  93. {
  94. return reason is not null ? $"[{reason}] {msg}" : msg;
  95. }
  96. }
  97. }
  98. catch
  99. {
  100. // ignore — fallback to raw body
  101. }
  102. return null;
  103. }
  104. private static string Truncate(string s, int max) =>
  105. s.Length <= max ? s : s[..max] + "...";
  106. }