This is my first time working with the YouTube Data API v3 and APIs in general.I have implemented the following requirements with the API in my existing web project:
- Retrieve a flexible top X of comments on a video, measured by the number of likes
- Retrieve all comments of a user on a video
- Retrieval of all comments on a video with a number X of likes
The data retrieval works smoothly in my IDE - Visual Studio.After testing everything for full functionality, I published the website extension. Now when I want to run the functions on the website, the page takes a long time to load and no results are returned.
The method seems to work in principle, at the end I am redirected. However, without any listing of the requested data.
My key is without restrictions, I prevent misuse via website authentication. I have already checked this. The quota limit has also not been reached. Enclosed are the measured values from the Google Cloud Center. I published the website on 10.12.2024 at around 22:20. I use IONOS Webhosting Essential Windows for my hosting.
YouTubeServices.cs
public class YouTubeServices { private readonly ILogger<YouTubeServices> _logger; public YouTubeServices(ILogger<YouTubeServices> logger) { _logger = logger; } private async Task<(string VideoTitle, int VideoViews)> GetVideoDetailsAsync(string videoId, string apiKey) { string videoInfoUrl = $"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&id={videoId}&key={apiKey}"; string videoTitle = null; int videoViews = 0; using (var client = new HttpClient()) { try { var videoInfoResponse = await client.GetAsync(videoInfoUrl); videoInfoResponse.EnsureSuccessStatusCode(); var videoInfoJson = await videoInfoResponse.Content.ReadAsStringAsync(); var videoInfoData = JObject.Parse(videoInfoJson); videoTitle = (string)videoInfoData["items"]?.FirstOrDefault()?["snippet"]?["title"]; videoViews = (int?)videoInfoData["items"]?.FirstOrDefault()?["statistics"]?["viewCount"] ?? 0; } catch (Exception ex) { _logger.LogError("Video info error: {0}", ex.Message); throw new Exception("Video details error"); } } return (videoTitle, videoViews); } private async Task<List<(string Author, string Comment, string CommentUrl, int Likes)>> GetCommentsAsync(string videoId, string apiKey, Func<JToken, bool> filter = null) { string apiUrl = $"https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId={videoId}&key={apiKey}"; List<(string Author, string Comment, string CommentUrl, int Likes)> comments = new(); string nextPageToken = null; using (var client = new HttpClient()) { try { do { string url = apiUrl + (nextPageToken != null ? $"&pageToken={nextPageToken}" : ""); var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); var jsonString = await response.Content.ReadAsStringAsync(); var jsonData = JObject.Parse(jsonString); var items = jsonData["items"]; foreach (var item in items) { var topLevelComment = item["snippet"]["topLevelComment"]["snippet"]; if (filter == null || filter(item)) { comments.Add(( Author: (string)topLevelComment["authorDisplayName"], Comment: RemoveHtmlTags((string)topLevelComment["textDisplay"]), CommentUrl: $"https://www.youtube.com/watch?v={videoId}&lc={(string)item["id"]}", Likes: (int)topLevelComment["likeCount"] )); } } nextPageToken = (string)jsonData["nextPageToken"]; } while (nextPageToken != null); } catch (Exception ex) { _logger.LogError("video comment error: {0}", ex.Message); } } return comments; } public async Task<(string VideoTitle, int VideoViews, List<(string Author, string Comment, string CommentUrl, int Likes)> Comments)> GetTopCommentsAsync(string videoUrl, string apiKey, int maxComments) { string videoId = ExtractVideoId(videoUrl); if (string.IsNullOrEmpty(videoId)) throw new ArgumentException("YouTube-URL not valid."); var (videoTitle, videoViews) = await GetVideoDetailsAsync(videoId, apiKey); var comments = await GetCommentsAsync(videoId, apiKey); return (videoTitle, videoViews, comments.OrderByDescending(c => c.Likes).Take(maxComments).ToList()); } public async Task<(string VideoTitle, int VideoViews, List<(string Author, string Comment, string CommentUrl, int Likes)> Comments)> GetCommentsByUserAsync(string videoUrl, string apiKey, string username) { string videoId = ExtractVideoId(videoUrl); if (string.IsNullOrEmpty(videoId)) throw new ArgumentException("YouTube-URL not valid."); var (videoTitle, videoViews) = await GetVideoDetailsAsync(videoId, apiKey); var comments = await GetCommentsAsync(videoId, apiKey, item => ((string)item["snippet"]["topLevelComment"]["snippet"]["authorDisplayName"]).Equals(username, StringComparison.OrdinalIgnoreCase) ); return (videoTitle, videoViews, comments); } public async Task<(string VideoTitle, int VideoViews, List<(string Author, string Comment, string CommentUrl, int Likes)> Comments)> GetCommentsByLikesAsync(string videoUrl, string apiKey, int exactLikes) { string videoId = ExtractVideoId(videoUrl); if (string.IsNullOrEmpty(videoId)) throw new ArgumentException("YouTube-URL not valid."); var (videoTitle, videoViews) = await GetVideoDetailsAsync(videoId, apiKey); var comments = await GetCommentsAsync(videoId, apiKey, item => (int)item["snippet"]["topLevelComment"]["snippet"]["likeCount"] == exactLikes ); return (videoTitle, videoViews, comments); } private static string ExtractVideoId(string videoUrl) { if (string.IsNullOrEmpty(videoUrl)) return null; var match = Regex.Match(videoUrl, @"(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})"); return match.Success ? match.Groups[1].Value : null; } private static string RemoveHtmlTags(string input) { if (string.IsNullOrEmpty(input)) return input; var allowedTags = new[] { "br", "b", "i", "u" }; return Regex.Replace(input, $@"</?(?!({string.Join("|", allowedTags)})\b)[^>]*>", string.Empty); } }ControlCenterController.cs:
public class ControlCenterController : Controller { private readonly ILogger<ControlCenterController> _logger; private readonly YouTubeServices _youTubeService; private readonly string _apiKey; public ControlCenterController( ILogger<ControlCenterController> logger, YouTubeServices youTubeService, IOptions<YouTubeConfig> youTubeConfig) { _logger = logger; _youTubeService = youTubeService; _apiKey = youTubeConfig.Value.ApiKey; } [Authorize(Roles = "Administrator,Editor")] [HttpGet] public async Task<IActionResult> Index() { CombinedIndexViewModel viewModel = await GetDefaultViewModelAsync(); return View(viewModel); } [Authorize(Roles = "Administrator,Editor")] [HttpPost] public async Task<IActionResult> ListCommentsByCount(string videoUrl, int? commentCount) { CombinedIndexViewModel viewModel = await GetDefaultViewModelAsync(); if (string.IsNullOrEmpty(videoUrl)) { ViewBag.ErrorMessage = "Enter a valid YouTube-URL."; return View("Index", viewModel); } try { int count = commentCount.GetValueOrDefault(int.MaxValue); var (videoTitle, videoViews, comments) = await _youTubeService.GetTopCommentsAsync(videoUrl, _apiKey, count); viewModel.YouTubeComments = comments.Select(comment => ( comment.Author, TransformLinks(comment.Comment), comment.CommentUrl, comment.Likes )).ToList(); viewModel.VideoTitle = videoTitle; viewModel.VideoViews = videoViews; return View("Index", viewModel); } catch (ArgumentException ex) { ViewBag.ErrorMessage = ex.Message; } catch (Exception ex) { _logger.LogError("Video comment error: {0}", ex.Message); ViewBag.ErrorMessage = "Video comment error."; } return View("Index", viewModel); } [Authorize(Roles = "Administrator,Editor")] [HttpPost] public async Task<IActionResult> ListCommentsByUser(string videoUrl, string username) { CombinedIndexViewModel viewModel = await GetDefaultViewModelAsync(); if (string.IsNullOrEmpty(videoUrl)) { ViewBag.ErrorMessage = "Enter a valid YouTube-URL."; return View("Index", viewModel); } if (string.IsNullOrEmpty(username)) { ViewBag.ErrorMessage = "Enter a valid YouTube username starting with @."; return View("Index", viewModel); } try { var (videoTitle, videoViews, comments) = await _youTubeService.GetCommentsByUserAsync(videoUrl, _apiKey, username); viewModel.VideoTitle = videoTitle; // Videotitel einfügen viewModel.VideoViews = videoViews; // Videoaufrufe einfügen viewModel.YouTubeComments = comments .Select(comment => ( comment.Author, TransformLinks(comment.Comment), comment.CommentUrl, comment.Likes )).ToList(); } catch (ArgumentException ex) { ViewBag.ErrorMessage = ex.Message; } catch (Exception ex) { _logger.LogError("Video comment error: {0}", ex.Message); ViewBag.ErrorMessage = "Video comment error"; } return View("Index", viewModel); } [Authorize(Roles = "Administrator,Editor")] [HttpPost] public async Task<IActionResult> ListCommentsByLikes(string videoUrl, int minLikes) { CombinedIndexViewModel viewModel = await GetDefaultViewModelAsync(); if (string.IsNullOrEmpty(videoUrl)) { ViewBag.ErrorMessage = "Enter a valid YouTube-URL."; return View("Index", viewModel); } if (minLikes < 0) { ViewBag.ErrorMessage = "Enter a number <= 0."; return View("Index", viewModel); } try { var (videoTitle, videoViews, comments) = await _youTubeService.GetCommentsByLikesAsync(videoUrl, _apiKey, minLikes); viewModel.VideoTitle = videoTitle; // Videotitel hinzufügen viewModel.VideoViews = videoViews; // Videoaufrufe hinzufügen viewModel.YouTubeComments = comments.Select(comment => ( comment.Author, TransformLinks(comment.Comment), comment.CommentUrl, comment.Likes )).ToList(); } catch (ArgumentException ex) { ViewBag.ErrorMessage = ex.Message; } catch (Exception ex) { _logger.LogError("Video comment error: {0}", ex.Message); ViewBag.ErrorMessage = "Video comment error"; } return View("Index", viewModel); } private async Task<CombinedIndexViewModel> GetDefaultViewModelAsync() { return new CombinedIndexViewModel { YouTubeComments = new List<(string Author, string Comment, string CommentUrl, int Likes)>() }; } // Methode zur Transformation von Links private static string TransformLinks(string input) { if (string.IsNullOrEmpty(input)) { return input; } // Präzisere Regex für URL-Erkennung mit verbatim string var urlRegex = new Regex(@"(https?://[^\s<>\""]+)"); // Ersetze Links durch anklickbares Wort und füge Zeilenumbruch hinzu string transformedText = urlRegex.Replace(input, match => { var url = match.Value; // Gefundene URL return $"<a href='{url}' target='_blank'>[Link]</a><br />"; }); // Ersetze Zeilenumbrüche mit <br /> return transformedText.Replace("\n", "").Replace("\r", ""); } }CombinedIndexViewModel.cs:
public List<(string Author, string Comment, string CommentUrl, int Likes)>? YouTubeComments { get; set; }public string? VideoTitle { get; set; }public int VideoViews { get; set; }appsettings.json:
{"ConnectionStrings": {"DefaultConnection": "deleted" },"Logging": {"LogLevel": {"Default": "Information","Microsoft.AspNetCore": "Warning" } },"AllowedHosts": "*","YouTube": {"ApiKey": "deleted" }}YouTubeConfig.cs:
public class YouTubeConfig { public string ApiKey { get; set; } = string.Empty; }What are my next step to solve the issue or even just understand why it's not working.