Skip to content

Commit

Permalink
Add RSS generation (partial)
Browse files Browse the repository at this point in the history
  • Loading branch information
taurit committed Aug 5, 2024
1 parent 91a0cc1 commit 4f074a7
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 25 deletions.
13 changes: 12 additions & 1 deletion AnkiStoryGenerator/AnkiStoryGenerator/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ private async void GenerateStory_OnClick(object sender, RoutedEventArgs e)
var storyHtmlWithTooltips = TooltipsHelper.AddInteractiveTooltipsMarkupToTheStory(story.OriginalHtml, _viewModel.Flashcards);
var translationHtmlWithTooltips = TooltipsHelper.AddInteractiveTooltipsMarkupToTheStory(story.TranslatedHtml, _viewModel.Flashcards);

this._viewModel.LatestStoryHtmlWithTooltips = storyHtmlWithTooltips;
this._viewModel.LatestTranslationHtmlWithTooltips = translationHtmlWithTooltips;

await SetPreviewWindowHtml(WebViewControlOriginal, storyHtmlWithTooltips);
await SetPreviewWindowHtml(WebViewControlTranslation, translationHtmlWithTooltips);

Expand Down Expand Up @@ -133,7 +136,15 @@ private async void PlayStory_OnClick(object sender, RoutedEventArgs e)

private void PublishToRssFeed_OnClick(object sender, RoutedEventArgs e)
{
new RssHelper().CreateFeed();
var podcastEpisode = new PodcastEpisode(
_viewModel.LatestStoryTitle,
_viewModel.LatestStoryHtmlWithTooltips,
_viewModel.LatestTranslationHtmlWithTooltips,
_viewModel.LatestStoryAudioFileName,
DateTimeOffset.Now);
var rssHelper = new RssHelper();
rssHelper.AddEpisodeToTheFeedInputData(podcastEpisode);
rssHelper.GenerateRssFeed();
}

private void Genre_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
Expand Down
4 changes: 4 additions & 0 deletions AnkiStoryGenerator/AnkiStoryGenerator/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ public class Settings
public static string AnkiDatabaseFilePath = ReturnFirstFileThatExists([
"c:\\Users\\windo\\AppData\\Roaming\\Anki2\\Usuario 1\\collection.anki2", // stationary pc
"c:\\Users\\windo\\AppData\\Roaming\\Anki2\\User 1\\collection.anki2" // dell laptop
]);

// rss feed file path
public static string RssFeedFolder = ReturnFirstDirectoryThatExists([
"s:\\Caches\\AnkiStoryGeneratorRssCache\\", // stationary pc
]);

// hardcoded for simplicity in the proof-of-concept phase
Expand Down
19 changes: 19 additions & 0 deletions AnkiStoryGenerator/AnkiStoryGenerator/Utilities/HtmlHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,23 @@ private static void ConvertTo(HtmlNode node, TextWriter outText)
break;
}
}

public static string ExtractTitleFromHtml(string? latestStoryHtml)
{
if (latestStoryHtml is null)
{
return "Title not found";
}

HtmlDocument doc = new();
doc.LoadHtml(latestStoryHtml);

var titleNode = doc.DocumentNode.SelectSingleNode("//h1");
if (titleNode is null)
{
return string.Empty;
}

return titleNode.InnerText;
}
}
89 changes: 65 additions & 24 deletions AnkiStoryGenerator/AnkiStoryGenerator/Utilities/RssHelper.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,69 @@
using System.ServiceModel.Syndication;
using System.IO;
using System.ServiceModel.Syndication;
using System.Text.Json;
using System.Xml;

namespace AnkiStoryGenerator.Utilities;

internal record PodcastEpisode(string StoryTitle, string StoryHtmlSpanish, string StoryHtmlPolish, string AudioSpanishFilePath, DateTimeOffset Created);

internal class RssHelper
{
internal void CreateFeed()
// Internal format to keep the state of all published episodes
private static readonly string EpisodesLocalFilePath = Path.Combine(Settings.RssFeedFolder, "episodes.json");

// A public feed of episodes, re-generated from scratch every time a new episode is added (rather than updated)
private static readonly string RssFeedLocalFilePath = Path.Combine(Settings.RssFeedFolder, "stories.xml");

internal void AddEpisodeToTheFeedInputData(PodcastEpisode podcastEpisode)
{
// load and deserialize the episodes from `EpisodesLocalFilePath`
var episodes = LoadPodcastEpisodesFromInternalDatabase();

// add the new episode to the list
episodes.Add(podcastEpisode);

// serialize and save the episodes back to `EpisodesLocalFilePath`
var serializedEpisodes = JsonSerializer.Serialize(episodes);
File.WriteAllText(EpisodesLocalFilePath, serializedEpisodes);
}


internal void GenerateRssFeed()
{
// Load episodes from internal database
var episodes = LoadPodcastEpisodesFromInternalDatabase();

// Create feed items (episodes)
var items = new List<SyndicationItem>
var rssItems = new List<SyndicationItem>();

foreach (var episode in episodes)
{
CreatePodcastItem(
title: "Episode 1",
description: "This is the first episode.",
url: "https://example.com/podcasts/episode1.mp3",
length: 12345678,
pubDate: DateTime.Now.AddDays(-10)
),
CreatePodcastItem(
title: "Episode 2",
description: "This is the second episode.",
url: "https://example.com/podcasts/episode2.mp3",
length: 23456789,
pubDate: DateTime.Now.AddDays(-5)
)
};
var newRssItem = CreatePodcastItem(
title: $"{episode.StoryTitle}",
description: episode.StoryHtmlSpanish,
url: episode.AudioSpanishFilePath,
length: new FileInfo(episode.AudioSpanishFilePath).Length,
pubDate: episode.Created.UtcDateTime
);
rssItems.Add(newRssItem);
}

// Create the podcast feed
var feed = new SyndicationFeed(
"My Podcast",
"A description of my podcast.",
"Anki Stories",
"A proof-of-concept feed for Anki Story Generator.",
new Uri("https://example.com/podcast"),
items
rssItems
)
{
Copyright = new TextSyndicationContent("© 2024 My Podcast"),
Language = "en-US",
Copyright = new TextSyndicationContent("© 2024 Taurit"),
Language = "es-ES",
LastUpdatedTime = DateTimeOffset.Now
};

// Generate the RSS XML
using (var writer = XmlWriter.Create("d:/podcast.xml"))
using (var writer = XmlWriter.Create(RssFeedLocalFilePath))
{
var rssFormatter = new Rss20FeedFormatter(feed);
rssFormatter.WriteTo(writer);
Expand All @@ -63,4 +87,21 @@ static SyndicationItem CreatePodcastItem(string title, string description, strin

return item;
}


private static List<PodcastEpisode> LoadPodcastEpisodesFromInternalDatabase()
{
var episodes = new List<PodcastEpisode>();
if (File.Exists(EpisodesLocalFilePath))
{
var json = File.ReadAllText(EpisodesLocalFilePath);
var episodesFromFile = JsonSerializer.Deserialize<List<PodcastEpisode>>(json);
if (episodesFromFile != null)
{
episodes.AddRange(episodesFromFile);
}
}

return episodes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ public class MainWindowViewModel

[DependsOn(nameof(LatestStoryPlainText))]
public string LatestStoryAudioFileName => Path.Combine(Settings.AudioFilesCacheDirectory, LatestStoryPlainText.GetHashCodeStable() + ".mp3");

public string LatestStoryHtmlWithTooltips { get; set; }

Check warning on line 30 in AnkiStoryGenerator/AnkiStoryGenerator/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LatestStoryHtmlWithTooltips' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 30 in AnkiStoryGenerator/AnkiStoryGenerator/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LatestStoryHtmlWithTooltips' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public string LatestTranslationHtmlWithTooltips { get; set; }

Check warning on line 31 in AnkiStoryGenerator/AnkiStoryGenerator/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LatestTranslationHtmlWithTooltips' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 31 in AnkiStoryGenerator/AnkiStoryGenerator/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'LatestTranslationHtmlWithTooltips' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

// Extract the title from the HTML content => the title is expected between <h1> and </h1> tags
[DependsOn(nameof(LatestStoryHtml))]
public string LatestStoryTitle => HtmlHelpers.ExtractTitleFromHtml(LatestStoryHtml);
}

[AddINotifyPropertyChangedInterface]
Expand Down

0 comments on commit 4f074a7

Please sign in to comment.