Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/atrefzer_tools.json
T

1 line
78 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "atrefzer/tools", "files": {"yt-digest/Program.cs": {"8ae0bc5a505bf89b67961ef4153996c695b6466c": {"t": 1785745808.218041, "n": 9043, "h": "85cc4f87ab5b64ee", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n // Simple format\n //var text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\temp\\\\QOE_uXsAc4I.de-DE.vtt\");\n\n // Karaoke format\n //text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\summaries\\\\a-Tq53g2Ows.de.vtt\");\n\n try\n {\n var options = CommandLineOptions.Parse(args);\n\n if (options.ShowHelp)\n {\n PrintHelp();\n return;\n }\n\n if (options.VideoUrl != null)\n {\n await PrintSingleVideoSummary(options);\n }\n else\n {\n var digestPath = await CreateDigestFile(options);\n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile(CommandLineOptions options)\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create(options.SummaryLang);\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService(AppPaths.TempDir);\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine($\"\\tMax videos : {options.MaxVideos}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, options.MaxVideos, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static void PrintHelp()\n {\n Console.WriteLine(\"\"\"\n YouTube Digest - Summarize YouTube videos using AI\n\n USAGE\n yt-digest Digest mode. Process all channels from channels.txt.\n Output to markdown file.\n yt-digest <url> Summarize a single video.\n Output to console.\n yt-digest --help Show this help\n\n OPTIONS\n --lang <code> Subtitle language to download (default: en)\n Examples: en, de\n --summary-lang <lang> Language for the generated summary\n Defaults to the transcript language\n Examples: German, English etc.\n --max-videos <n> Max videos to fetch per channel in digest mode\n Default: 2\n\n EXAMPLES\n yt-digest\n yt-digest https://www.youtube.com/watch?v=VIDEO_ID\n yt-digest https://www.youtube.com/watch?v=VIDEO_ID --lang de\n yt-digest https://www.youtube.com/watch?v=VIDEO_ID --summary-lang German\n yt-digest --summary-lang German\n yt-digest --max-videos 5\n yt-digest https://... --lang en --summary-lang German > summary.md\n\n CONFIGURATION\n All files are stored in the app data directory:\n Windows : %LOCALAPPDATA%\\yt-digest\\\n Linux : ~/.local/share/yt-digest/\n macOS : ~/Library/Application Support/yt-digest/\n\n channels.txt Channels to process in digest mode (one URL per line, optional ,lang suffix)\n claude.key Anthropic Claude API key\n deepseek.key DeepSeek API key\n\n AI BACKEND PRIORITY\n 1. claude.key / ANTHROPIC_API_KEY\n 2. deepseek.key / DEEPSEEK_API_KEY\n 3. Ollama (local fallback, http://localhost:11434)\n \"\"\");\n }\n\n private static async Task PrintSingleVideoSummary(CommandLineOptions options)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create(options.SummaryLang);\n var ytDlp = new YtDlpService(AppPaths.TempDir);\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(options.VideoUrl!);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({options.SubtitleLang})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, options.SubtitleLang);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Out.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n int maxVideos,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {maxVideos} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, maxVideos);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(maxVideos)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}\n"}, "d416b4b24ca1b2cde2eb232a464eca1f677a21a0": {"t": 1785756735.6795557, "n": 9043, "h": "85cc4f87ab5b64ee", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n // Simple format\n //var text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\temp\\\\QOE_uXsAc4I.de-DE.vtt\");\n\n // Karaoke format\n //text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\summaries\\\\a-Tq53g2Ows.de.vtt\");\n\n try\n {\n var options = CommandLineOptions.Parse(args);\n\n if (options.ShowHelp)\n {\n PrintHelp();\n return;\n }\n\n if (options.VideoUrl != null)\n {\n await PrintSingleVideoSummary(options);\n }\n else\n {\n var digestPath = await CreateDigestFile(options);\n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile(CommandLineOptions options)\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create(options.SummaryLang);\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService(AppPaths.TempDir);\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine($\"\\tMax videos : {options.MaxVideos}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, options.MaxVideos, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static void PrintHelp()\n {\n Console.WriteLine(\"\"\"\n YouTube Digest - Summarize YouTube videos using AI\n\n USAGE\n yt-digest Digest mode. Process all channels from channels.txt.\n Output to markdown file.\n yt-digest <url> Summarize a single video.\n Output to console.\n yt-digest --help Show this help\n\n OPTIONS\n --lang <code> Subtitle language to download (default: en)\n Examples: en, de\n --summary-lang <lang> Language for the generated summary\n Defaults to the transcript language\n Examples: German, English etc.\n --max-videos <n> Max videos to fetch per channel in digest mode\n Default: 2\n\n EXAMPLES\n yt-digest\n yt-digest https://www.youtube.com/watch?v=VIDEO_ID\n yt-digest https://www.youtube.com/watch?v=VIDEO_ID --lang de\n yt-digest https://www.youtube.com/watch?v=VIDEO_ID --summary-lang German\n yt-digest --summary-lang German\n yt-digest --max-videos 5\n yt-digest https://... --lang en --summary-lang German > summary.md\n\n CONFIGURATION\n All files are stored in the app data directory:\n Windows : %LOCALAPPDATA%\\yt-digest\\\n Linux : ~/.local/share/yt-digest/\n macOS : ~/Library/Application Support/yt-digest/\n\n channels.txt Channels to process in digest mode (one URL per line, optional ,lang suffix)\n claude.key Anthropic Claude API key\n deepseek.key DeepSeek API key\n\n AI BACKEND PRIORITY\n 1. claude.key / ANTHROPIC_API_KEY\n 2. deepseek.key / DEEPSEEK_API_KEY\n 3. Ollama (local fallback, http://localhost:11434)\n \"\"\");\n }\n\n private static async Task PrintSingleVideoSummary(CommandLineOptions options)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create(options.SummaryLang);\n var ytDlp = new YtDlpService(AppPaths.TempDir);\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(options.VideoUrl!);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({options.SubtitleLang})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, options.SubtitleLang);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Out.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n int maxVideos,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {maxVideos} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, maxVideos);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(maxVideos)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}\n"}, "ce9f962801679d64884a616f076eca4d29b8b68f": {"t": 1785756737.1627889, "n": 7600, "h": "39df02a34eda4d76", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n // Simple format\n //var text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\temp\\\\QOE_uXsAc4I.de-DE.vtt\");\n\n // Karaoke format\n //text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\summaries\\\\a-Tq53g2Ows.de.vtt\");\n\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language, summaryLanguage) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language, summaryLanguage);\n }\n else\n {\n var digestPath = await CreateDigestFile(summaryLanguage);\n \n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile(string? summaryLanguage = null)\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create(summaryLanguage);\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService(AppPaths.TempDir);\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language, string? summaryLanguage) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n\n var summaryLangIndex = Array.IndexOf(args, \"--summary-lang\");\n var summaryLanguage = summaryLangIndex >= 0 && summaryLangIndex + 1 < args.Length\n ? args[summaryLangIndex + 1]\n : null;\n\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language, summaryLanguage);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language, string? summaryLanguage)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create(summaryLanguage);\n\n\n var ytDlp = new YtDlpService(AppPaths.TempDir);\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Out.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(Constants.MaxVideosPerChannel)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}, "c0c6b7fa6229eff849062d68268352a3b1b3b97e": {"t": 1785756738.4312584, "n": 7201, "h": "6fa9274b7a5863fe", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n // Simple format\n //var text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\temp\\\\QOE_uXsAc4I.de-DE.vtt\");\n\n // Karaoke format\n //text = VttParser.ParseFile(\"C:\\\\Users\\\\ATrefzer\\\\AppData\\\\Local\\\\yt-digest\\\\summaries\\\\a-Tq53g2Ows.de.vtt\");\n\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language);\n }\n else\n {\n var digestPath = await CreateDigestFile();\n \n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile()\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create();\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService(AppPaths.TempDir);\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create();\n\n\n var ytDlp = new YtDlpService(AppPaths.TempDir);\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Out.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(Constants.MaxVideosPerChannel)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}, "60e7d5e14a1ec1214c473a7b98612159633a4d27": {"t": 1785756739.967534, "n": 6908, "h": "1cd09cef5e5063c5", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language);\n }\n else\n {\n var digestPath = await CreateDigestFile();\n \n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile()\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create();\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService(AppPaths.TempDir);\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create();\n\n\n var ytDlp = new YtDlpService(AppPaths.TempDir);\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Out.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(Constants.MaxVideosPerChannel)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}, "f731fbe449ede8d5c0092e42888824a928b64e18": {"t": 1785756741.4859746, "n": 6876, "h": "4a42813ac53568a6", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language);\n }\n else\n {\n var digestPath = await CreateDigestFile();\n \n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile()\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create();\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService();\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create();\n\n\n var ytDlp = new YtDlpService();\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Out.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(Constants.MaxVideosPerChannel)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}, "b7882c4c7e2335781b591931a6fb1b74a3ed472c": {"t": 1785756742.8663151, "n": 6878, "h": "371d4a0b138789a5", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language);\n }\n else\n {\n var digestPath = await CreateDigestFile();\n \n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile()\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create();\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService();\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create();\n\n\n var ytDlp = new YtDlpService();\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Error.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang]\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos\n .Where(v => !store.IsProcessed(v.Id))\n .Take(Constants.MaxVideosPerChannel)\n .ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}, "2e3835e21ebd61ccf6952221c833c2ee9acb3f1d": {"t": 1785756744.2673118, "n": 6803, "h": "27990ed07c99dcf1", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language);\n }\n else\n {\n var digestPath = await CreateDigestFile();\n \n\n if (!string.IsNullOrEmpty(digestPath))\n {\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile()\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create();\n var store = new SummaryStore(AppPaths.ProcessedVideosFile);\n var ytService = new YtDlpService();\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create();\n\n\n var ytDlp = new YtDlpService();\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Error.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(AppPaths.ChannelsFile))\n {\n await File.WriteAllTextAsync(AppPaths.ChannelsFile,\n \"# Add one YouTube channel URL per line.\\n# Example:\\n# https://www.youtube.com/@SomeChannel/videos[,subtitle-lang}\\n\");\n Console.WriteLine($\"Created channels file: {AppPaths.ChannelsFile}\");\n Console.WriteLine(\"\\tAdd YouTube channel URLs and run again.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(AppPaths.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {AppPaths.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos.Where(v => !store.IsProcessed(v.Id)).ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(AppPaths.SummariesDir);\n var outputPath = Path.Combine(AppPaths.SummariesDir, $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}, "2cad654889848d73ff51c1cca9f2a8b1ed784dfe": {"t": 1785756745.7022488, "n": 6615, "h": "31eb04841376cf10", "c": "using System.Diagnostics;\nusing YoutubeDigest.Models;\nusing YoutubeDigest.Services;\n\nnamespace YoutubeDigest;\n\ninternal class Program\n{\n public static async Task Main(string[] args)\n {\n try\n {\n // Single-video mode: yt-digest <url> [--lang <language>]\n var (videoUrl, language) = ParseCommandLine(args);\n if (videoUrl != null)\n {\n await PrintSingleVideoSummary(videoUrl, language);\n }\n else\n {\n var digestPath = await CreateDigestFile();\n Console.WriteLine($\"Digest saved to: {digestPath}\");\n\n if (!string.IsNullOrEmpty(digestPath))\n {\n OpenFile(digestPath);\n }\n }\n }\n catch (Exception e)\n {\n await Console.Error.WriteLineAsync(e.ToString());\n }\n }\n\n private static async Task<string> CreateDigestFile()\n {\n var channels = await LoadChannelsAsync();\n if (channels.Count == 0)\n {\n return string.Empty;\n }\n\n var summaryService = SummaryServiceFactory.Create();\n var store = new SummaryStore(\"processed_videos.json\");\n var ytService = new YtDlpService();\n var digest = new DigestBuilder();\n\n Console.WriteLine(\"YouTube Digest started\");\n Console.WriteLine($\"\\tAI : {summaryService.ModelName}\");\n Console.WriteLine($\"\\tChannels : {channels.Count}\");\n Console.WriteLine();\n\n foreach (var channel in channels)\n {\n await ProcessChannelAsync(channel, ytService, summaryService, store, digest);\n }\n\n store.UpdateLastRun();\n\n var digestPath = await digest.SaveAsync();\n\n Console.WriteLine($\"Done! {digest.Count} videos processed.\");\n\n return digestPath;\n }\n\n\n public static void OpenFile(string path)\n {\n try\n {\n Process.Start(new ProcessStartInfo\n {\n FileName = path,\n UseShellExecute = true\n });\n }\n catch (Exception ex)\n {\n Console.WriteLine($\"Could not open digest file: {ex.Message}\");\n }\n }\n\n private static (string? videoUrl, string language) ParseCommandLine(string[] args)\n {\n var langIndex = Array.IndexOf(args, \"--lang\");\n var language = langIndex >= 0 && langIndex + 1 < args.Length ? args[langIndex + 1] : \"en\";\n var videoUrl =\n args.FirstOrDefault(a => a.StartsWith(\"http://\") || a.StartsWith(\"https://\") || a.StartsWith(\"www.\"));\n return (videoUrl, language);\n }\n\n private static async Task PrintSingleVideoSummary(string videoUrl, string language)\n {\n // Info messages are written to stderr to keep stdout clean for potential redirection.\n\n var summaryService = SummaryServiceFactory.Create();\n\n\n var ytDlp = new YtDlpService();\n\n await Console.Error.WriteLineAsync(\"Fetching video info...\");\n var video = await ytDlp.GetVideoInfoAsync(videoUrl);\n\n await Console.Error.WriteLineAsync($\"Downloading subtitles ({language})...\");\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"No subtitles available for this video.\");\n return;\n }\n\n await Console.Error.WriteLineAsync(\"Generating summary...\");\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Redirect to a file if needed.\n await Console.Error.WriteLineAsync(summary);\n }\n\n private static async Task<List<ChannelConfig>> LoadChannelsAsync()\n {\n if (!File.Exists(Constants.ChannelsFile))\n {\n Console.WriteLine($\"Error: Config file not found: {Constants.ChannelsFile}\");\n Console.WriteLine(\"\\tCreate this file with one YouTube channel URL per line.\");\n return [];\n }\n\n var channels = (await File.ReadAllLinesAsync(Constants.ChannelsFile))\n .Select(line => line.Trim())\n .Where(line => !string.IsNullOrEmpty(line) && !line.StartsWith(\"#\"))\n .Select(ChannelConfig.Parse)\n .ToList();\n\n if (channels.Count == 0)\n {\n Console.WriteLine($\"Error: No channels configured in {Constants.ChannelsFile}\");\n }\n\n return channels;\n }\n\n private static async Task ProcessChannelAsync(\n ChannelConfig channel,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"Channel: {channel.Url} ({channel.Language})\");\n Console.WriteLine($\"\\tFetching last {Constants.MaxVideosPerChannel} videos...\");\n\n var allVideos = await ytDlp.GetRecentVideosAsync(channel.Url, Constants.MaxVideosPerChannel);\n var newVideos = allVideos.Where(v => !store.IsProcessed(v.Id)).ToList();\n\n Console.WriteLine($\"\\t{newVideos.Count} new videos to process.\\n\");\n\n if (newVideos.Count > 0)\n {\n digest.AddChannelHeader(channel.Name);\n }\n\n foreach (var video in newVideos)\n {\n await ProcessVideoAsync(video, channel.Language, ytDlp, summaryService, store, digest);\n }\n }\n\n private static async Task ProcessVideoAsync(\n VideoInfo video,\n string language,\n YtDlpService ytDlp,\n ISummaryService summaryService,\n SummaryStore store,\n DigestBuilder digest)\n {\n Console.WriteLine($\"\\t\\tProcessing: {video.Title}\");\n\n var transcript = await ytDlp.DownloadSubtitlesAsync(video.Id, language);\n if (string.IsNullOrWhiteSpace(transcript))\n {\n Console.WriteLine(\"\\t\\t(!) No subtitles available – skipping.\\n\");\n store.MarkProcessed(video.Id, video.Title);\n return;\n }\n\n Console.WriteLine($\"\\t\\tTranscript: {transcript.Length:N0} characters\");\n Console.WriteLine(\"\\t\\tGenerating summary...\");\n\n var summary = await summaryService.SummarizeAsync(video, transcript);\n\n // Save individual summary\n Directory.CreateDirectory(\"summaries\");\n var outputPath = Path.Combine(\"summaries\", $\"{video.Id}.md\");\n await File.WriteAllTextAsync(outputPath, summary);\n\n // Add to digest and mark as processed\n digest.Add(summary);\n store.MarkProcessed(video.Id, video.Title);\n\n Console.WriteLine($\"\\t\\tSaved to: {outputPath}\\n\");\n }\n}"}}, "yt-digest/Services/SummaryServiceFactory.cs": {"8ae0bc5a505bf89b67961ef4153996c695b6466c": {"t": 1785745822.2139888, "n": 1343, "h": "0b76e5e0a8e1216e", "c": "namespace YoutubeDigest.Services;\n\n/// <summary>\n/// Factory to create the appropriate summary service based on configuration.\n/// </summary>\npublic static class SummaryServiceFactory\n{\n /// <summary>\n /// Creates a summary service based on available configuration.\n /// Priority:\n /// 1 claude.key file\n /// 2 ANTHROPIC_API_KEY env var\n /// 3 deepseek.key file\n /// 4 DEEPSEEK_API_KEY env var\n /// 5 Ollama fallback\n /// </summary>\n public static ISummaryService Create(string? summaryLanguage = null)\n {\n var prompt = new Prompt(summaryLanguage);\n\n var claudeKey = LoadKey(AppPaths.ClaudeKeyFile) ?? Environment.GetEnvironmentVariable(\"ANTHROPIC_API_KEY\");\n if (!string.IsNullOrEmpty(claudeKey))\n return new ClaudeService(claudeKey, prompt);\n\n var deepSeekKey = LoadKey(AppPaths.DeepSeekKeyFile) ?? Environment.GetEnvironmentVariable(\"DEEPSEEK_API_KEY\");\n if (!string.IsNullOrEmpty(deepSeekKey))\n return new DeepSeekService(deepSeekKey, prompt);\n\n return new OllamaService(prompt);\n }\n\n private static string? LoadKey(string path)\n {\n if (!File.Exists(path))\n return null;\n\n var key = File.ReadAllText(path).Trim();\n return string.IsNullOrEmpty(key) ? null : key;\n }\n}"}, "ce9f962801679d64884a616f076eca4d29b8b68f": {"t": 1785756734.5463831, "n": 1343, "h": "0b76e5e0a8e1216e", "c": "namespace YoutubeDigest.Services;\n\n/// <summary>\n/// Factory to create the appropriate summary service based on configuration.\n/// </summary>\npublic static class SummaryServiceFactory\n{\n /// <summary>\n /// Creates a summary service based on available configuration.\n /// Priority:\n /// 1 claude.key file\n /// 2 ANTHROPIC_API_KEY env var\n /// 3 deepseek.key file\n /// 4 DEEPSEEK_API_KEY env var\n /// 5 Ollama fallback\n /// </summary>\n public static ISummaryService Create(string? summaryLanguage = null)\n {\n var prompt = new Prompt(summaryLanguage);\n\n var claudeKey = LoadKey(AppPaths.ClaudeKeyFile) ?? Environment.GetEnvironmentVariable(\"ANTHROPIC_API_KEY\");\n if (!string.IsNullOrEmpty(claudeKey))\n return new ClaudeService(claudeKey, prompt);\n\n var deepSeekKey = LoadKey(AppPaths.DeepSeekKeyFile) ?? Environment.GetEnvironmentVariable(\"DEEPSEEK_API_KEY\");\n if (!string.IsNullOrEmpty(deepSeekKey))\n return new DeepSeekService(deepSeekKey, prompt);\n\n return new OllamaService(prompt);\n }\n\n private static string? LoadKey(string path)\n {\n if (!File.Exists(path))\n return null;\n\n var key = File.ReadAllText(path).Trim();\n return string.IsNullOrEmpty(key) ? null : key;\n }\n}"}, "28650a39f586cfc4b033e3cb77496da1f129e3dc": {"t": 1785756735.82261, "n": 1298, "h": "b4bc7bd9640d0daf", "c": "namespace YoutubeDigest.Services;\n\n/// <summary>\n/// Factory to create the appropriate summary service based on configuration.\n/// </summary>\npublic static class SummaryServiceFactory\n{\n /// <summary>\n /// Creates a summary service based on available configuration.\n /// Priority:\n /// 1 claude.key file\n /// 2 ANTHROPIC_API_KEY env var\n /// 3 deepseek.key file\n /// 4 DEEPSEEK_API_KEY env var\n /// 5 Ollama fallback\n /// </summary>\n public static ISummaryService Create()\n {\n var prompt = new Prompt();\n\n var claudeKey = LoadKey(AppPaths.ClaudeKeyFile) ?? Environment.GetEnvironmentVariable(\"ANTHROPIC_API_KEY\");\n if (!string.IsNullOrEmpty(claudeKey))\n return new ClaudeService(claudeKey, prompt);\n\n var deepSeekKey = LoadKey(AppPaths.DeepSeekKeyFile) ?? Environment.GetEnvironmentVariable(\"DEEPSEEK_API_KEY\");\n if (!string.IsNullOrEmpty(deepSeekKey))\n return new DeepSeekService(deepSeekKey, prompt);\n\n return new OllamaService(prompt);\n }\n\n private static string? LoadKey(string path)\n {\n if (!File.Exists(path))\n return null;\n\n var key = File.ReadAllText(path).Trim();\n return string.IsNullOrEmpty(key) ? null : key;\n }\n}"}, "b7882c4c7e2335781b591931a6fb1b74a3ed472c": {"t": 1785756737.3417857, "n": 1331, "h": "dd1a797dcd5d1f53", "c": "namespace YoutubeDigest.Services;\n\n/// <summary>\n/// Factory to create the appropriate summary service based on configuration.\n/// </summary>\npublic static class SummaryServiceFactory\n{\n private static readonly string KeyFile = AppPaths.KeyFile;\n\n /// <summary>\n /// Creates a summary service based on available configuration.\n /// Priority: 1) key.txt file, 2) ANTHROPIC_API_KEY env var, 3) Ollama fallback\n /// </summary>\n public static ISummaryService Create()\n {\n var promptBuilder = new Prompt();\n var anthropicApiKey = LoadAnthropicApiKey();\n\n if (!string.IsNullOrEmpty(anthropicApiKey))\n {\n var service = new ClaudeService(anthropicApiKey, promptBuilder);\n return service;\n }\n else\n {\n var service = new OllamaService(promptBuilder);\n return service;\n }\n }\n\n private static string? LoadAnthropicApiKey()\n {\n // 1. Try key.txt file first\n if (File.Exists(KeyFile))\n {\n var key = File.ReadAllText(KeyFile).Trim();\n if (!string.IsNullOrEmpty(key))\n {\n return key;\n }\n }\n\n // 2. Fall back to environment variable\n return Environment.GetEnvironmentVariable(\"ANTHROPIC_API_KEY\");\n }\n}"}, "2e3835e21ebd61ccf6952221c833c2ee9acb3f1d": {"t": 1785756738.641756, "n": 1314, "h": "826e2622d580069b", "c": "using YoutubeDigest;\n\nnamespace YoutubeDigest.Services;\n\n/// <summary>\n/// Factory to create the appropriate summary service based on configuration.\n/// </summary>\npublic static class SummaryServiceFactory\n{\n private static readonly string KeyFile = AppPaths.KeyFile;\n\n /// <summary>\n /// Creates a summary service based on available configuration.\n /// Priority: 1) key.txt file, 2) ANTHROPIC_API_KEY env var, 3) Ollama fallback\n /// </summary>\n public static ISummaryService Create()\n {\n var promptBuilder = new Prompt();\n var anthropicApiKey = LoadAnthropicApiKey();\n\n if (!string.IsNullOrEmpty(anthropicApiKey))\n {\n var service = new ClaudeService(anthropicApiKey, promptBuilder);\n return service;\n }\n else\n {\n var service = new OllamaService(promptBuilder);\n return service;\n }\n }\n\n private static string? LoadAnthropicApiKey()\n {\n // 1. Try key.txt file first\n if (File.Exists(KeyFile))\n {\n var key = File.ReadAllText(KeyFile).Trim();\n if (!string.IsNullOrEmpty(key))\n return key;\n }\n\n // 2. Fall back to environment variable\n return Environment.GetEnvironmentVariable(\"ANTHROPIC_API_KEY\");\n }\n}\n"}, "2cad654889848d73ff51c1cca9f2a8b1ed784dfe": {"t": 1785756740.1214976, "n": 1275, "h": "1a358187e2948b81", "c": "namespace YoutubeDigest.Services;\n\n/// <summary>\n/// Factory to create the appropriate summary service based on configuration.\n/// </summary>\npublic static class SummaryServiceFactory\n{\n private const string KeyFile = \"key.txt\";\n\n /// <summary>\n /// Creates a summary service based on available configuration.\n /// Priority: 1) key.txt file, 2) ANTHROPIC_API_KEY env var, 3) Ollama fallback\n /// </summary>\n public static ISummaryService Create()\n {\n var promptBuilder = new Prompt();\n var anthropicApiKey = LoadAnthropicApiKey();\n\n if (!string.IsNullOrEmpty(anthropicApiKey))\n {\n var service = new ClaudeService(anthropicApiKey, promptBuilder);\n return service;\n }\n else\n {\n var service = new OllamaService(promptBuilder);\n return service;\n }\n }\n\n private static string? LoadAnthropicApiKey()\n {\n // 1. Try key.txt file first\n if (File.Exists(KeyFile))\n {\n var key = File.ReadAllText(KeyFile).Trim();\n if (!string.IsNullOrEmpty(key))\n return key;\n }\n\n // 2. Fall back to environment variable\n return Environment.GetEnvironmentVariable(\"ANTHROPIC_API_KEY\");\n }\n}\n"}}}}