Created
November 4, 2025 20:33
-
-
Save AldeRoberge/4a70a5e9aeb375fdc84a29abf5e7355e to your computer and use it in GitHub Desktop.
Allows to find a list of YouTube videos based on their titles.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Net.Http; | |
| using System.Text.Json; | |
| using System.Text.RegularExpressions; | |
| using System.Threading.Tasks; | |
| using System.Web; | |
| namespace YouTubeFinder | |
| { | |
| public class Program | |
| { | |
| private static readonly HttpClient httpClient = new HttpClient | |
| { | |
| Timeout = TimeSpan.FromSeconds(15) | |
| }; | |
| public static async Task Main(string[] args) | |
| { | |
| Console.ForegroundColor = ConsoleColor.Cyan; | |
| Console.WriteLine("=== YouTube Finder: Web Search Mode ==="); | |
| Console.ResetColor(); | |
| var videoTitles = new[] | |
| { | |
| "1 critical Blazor mistake to avoid", | |
| "Vertical slice architecture — Patrick God", | |
| "How to catch a cheater with math", | |
| "Macroblank Serpent EP (music)", | |
| "How to deploy your application to Azure using GitHub (Milan)" | |
| }; | |
| var results = new List<SearchResult>(); | |
| foreach (var title in videoTitles) | |
| { | |
| LogInfo($"Starting search for: \"{title}\""); | |
| var result = await SearchYouTubeViaWebAsync(title); | |
| results.Add(result); | |
| LogSuccess($"Finished: \"{title}\" -> {result.Url ?? "❌ No result"} (confidence {result.Confidence:0.00})"); | |
| Console.WriteLine(new string('-', 80)); | |
| await Task.Delay(2000); // delay to avoid rate limits | |
| } | |
| LogInfo("All searches complete. Generating JSON output..."); | |
| string jsonOutput = JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }); | |
| Console.ForegroundColor = ConsoleColor.Yellow; | |
| Console.WriteLine(jsonOutput); | |
| Console.ResetColor(); | |
| } | |
| private static async Task<SearchResult> SearchYouTubeViaWebAsync(string query) | |
| { | |
| var encodedQuery = Uri.EscapeDataString($"{query} site:youtube.com"); | |
| var searchUrl = $"https://www.bing.com/search?q={encodedQuery}"; | |
| var result = new SearchResult | |
| { | |
| Query = query, | |
| Method = "WebSearch", | |
| Confidence = 0.0, | |
| Notes = "" | |
| }; | |
| try | |
| { | |
| LogDebug($"Requesting Bing search for query: {searchUrl}"); | |
| using var response = await httpClient.GetAsync(searchUrl); | |
| if (!response.IsSuccessStatusCode) | |
| { | |
| LogError($"Search request failed ({response.StatusCode}) for query: {query}"); | |
| result.Notes = $"HTTP error: {response.StatusCode}"; | |
| return result; | |
| } | |
| var html = await response.Content.ReadAsStringAsync(); | |
| LogDebug($"Downloaded {html.Length} characters of HTML."); | |
| // --- extract both direct and redirect-style links --- | |
| var matches = new List<string>(); | |
| // Direct links | |
| matches.AddRange(Regex.Matches(html, @"https://www\.youtube\.com/watch\?v=[\w-]{11}") | |
| .Cast<Match>().Select(m => m.Value)); | |
| // Redirected links (Bing uses /l?url=...) | |
| var redirectMatches = Regex.Matches(html, @"\/l\?url=(https%3[a-zA-Z0-9%._\-]+youtube\.com[a-zA-Z0-9%._\-&=?]+)") | |
| .Cast<Match>() | |
| .Select(m => HttpUtility.UrlDecode(m.Groups[1].Value)) | |
| .Where(u => u.Contains("youtube.com/watch")) | |
| .ToList(); | |
| matches.AddRange(redirectMatches); | |
| var distinctUrls = matches | |
| .Select(u => u.Split('&')[0]) // remove tracking params | |
| .Distinct() | |
| .ToList(); | |
| LogInfo($"Found {distinctUrls.Count} YouTube link(s) in search results:"); | |
| foreach (var link in distinctUrls) | |
| LogDebug($" ↳ {link}"); | |
| if (distinctUrls.Count > 0) | |
| { | |
| result.Url = distinctUrls.First(); | |
| result.Confidence = distinctUrls.Count > 3 ? 0.8 : 0.7; | |
| result.MatchTitle = query; | |
| result.Notes = $"Found {distinctUrls.Count} candidate(s); selected top result."; | |
| } | |
| else | |
| { | |
| result.Url = null; | |
| result.Confidence = 0.0; | |
| result.Notes = "No YouTube result found."; | |
| } | |
| } | |
| catch (TaskCanceledException) | |
| { | |
| LogError($"Timeout while searching for \"{query}\""); | |
| result.Notes = "Timeout occurred."; | |
| } | |
| catch (Exception ex) | |
| { | |
| LogError($"Exception while searching for \"{query}\": {ex.Message}"); | |
| result.Notes = $"Error: {ex.Message}"; | |
| } | |
| return result; | |
| } | |
| // ----- Logging Helpers ----- | |
| private static void LogInfo(string message) | |
| { | |
| Console.ForegroundColor = ConsoleColor.Cyan; | |
| Console.WriteLine($"[INFO] {DateTime.Now:T} | {message}"); | |
| Console.ResetColor(); | |
| } | |
| private static void LogDebug(string message) | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.WriteLine($"[DEBUG] {DateTime.Now:T} | {message}"); | |
| Console.ResetColor(); | |
| } | |
| private static void LogSuccess(string message) | |
| { | |
| Console.ForegroundColor = ConsoleColor.Green; | |
| Console.WriteLine($"[SUCCESS] {DateTime.Now:T} | {message}"); | |
| Console.ResetColor(); | |
| } | |
| private static void LogError(string message) | |
| { | |
| Console.ForegroundColor = ConsoleColor.Red; | |
| Console.WriteLine($"[ERROR] {DateTime.Now:T} | {message}"); | |
| Console.ResetColor(); | |
| } | |
| } | |
| public class SearchResult | |
| { | |
| public string Query { get; set; } | |
| public string Url { get; set; } | |
| public double Confidence { get; set; } | |
| public string Method { get; set; } | |
| public string MatchTitle { get; set; } | |
| public string PublishDate { get; set; } | |
| public string Notes { get; set; } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment