{"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 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 Summarize a single video.\n Output to console.\n yt-digest --help Show this help\n\n OPTIONS\n --lang Subtitle language to download (default: en)\n Examples: en, de\n --summary-lang Language for the generated summary\n Defaults to the transcript language\n Examples: German, English etc.\n --max-videos 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> 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 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 Summarize a single video.\n Output to console.\n yt-digest --help Show this help\n\n OPTIONS\n --lang Subtitle language to download (default: en)\n Examples: en, de\n --summary-lang Language for the generated summary\n Defaults to the transcript language\n Examples: German, English etc.\n --max-videos 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> 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 [--lang ]\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 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> 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 [--lang ]\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 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> 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 [--lang ]\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 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> 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 [--lang ]\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 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> 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 [--lang ]\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 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> 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 [--lang ]\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 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> 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 [--lang ]\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 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> 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/// \n/// Factory to create the appropriate summary service based on configuration.\n/// \npublic static class SummaryServiceFactory\n{\n /// \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 /// \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/// \n/// Factory to create the appropriate summary service based on configuration.\n/// \npublic static class SummaryServiceFactory\n{\n /// \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 /// \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/// \n/// Factory to create the appropriate summary service based on configuration.\n/// \npublic static class SummaryServiceFactory\n{\n /// \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 /// \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/// \n/// Factory to create the appropriate summary service based on configuration.\n/// \npublic static class SummaryServiceFactory\n{\n private static readonly string KeyFile = AppPaths.KeyFile;\n\n /// \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 /// \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/// \n/// Factory to create the appropriate summary service based on configuration.\n/// \npublic static class SummaryServiceFactory\n{\n private static readonly string KeyFile = AppPaths.KeyFile;\n\n /// \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 /// \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/// \n/// Factory to create the appropriate summary service based on configuration.\n/// \npublic static class SummaryServiceFactory\n{\n private const string KeyFile = \"key.txt\";\n\n /// \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 /// \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"}}}}