Podcast episodes play; servers with NodeInfo2 only are described

Castopod announces an episode with a Note that only links to the episode's page, which serves a PodcastEpisode. PrivaPub
reads PodcastEpisode (its words in description, its sound in audio, its cover in image), and a Note that is only a link
to its author's own episode takes the episode's sound, title, cover and words, so the post plays. A server that
publishes no NodeInfo is described from NodeInfo2 (/.well-known/x-nodeinfo2), as Castopod publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 11:12:05 +02:00
1 parent 3df3c333bf
commit 69ab8e50eb
7 files changed
+163 -3

No files matched your search

+26 -1
View File
@@ -83,7 +83,7 @@ namespace PrivaPub.Federation.Inbox
{
var mentions = await Mentions(note, to.Concat(cc), _localActors, token);
return new PostEntity
var post = new PostEntity
{
ID = PrivacyIds.Arrived(note.Published),
ObjectURI = note.Id,
@@ -135,6 +135,31 @@ namespace PrivaPub.Federation.Inbox
UpdateDate = note.Updated ?? note.Published,
EditedAt = note.Updated
};
await Episode(post, note, author, token);
return post;
}
static readonly System.Text.RegularExpressions.Regex Href = new(@"href=""([^""]+)""", System.Text.RegularExpressions.RegexOptions.Compiled);
// a post that is only a link to a podcast episode of its own author (Castopod announces each episode so): the
// episode's sound, title and cover, read from it
async Task Episode(PostEntity post, NoteDocument note, ForeignAvatar author, CancellationToken token)
{
if (note.Type != "Note" || post.Media.Count > 0 || string.IsNullOrEmpty(note.ContentHtml))
return;
var links = Href.Matches(note.ContentHtml).Select(m => System.Net.WebUtility.HtmlDecode(m.Groups[1].Value)).Distinct().ToList();
if (links.Count != 1 || links[0] == note.Id || !Origin.Same(links[0], author.ActorURI))
return;
using var scope = HttpScope.For("object");
using var fetched = await _remoteActors.FetchObject(links[0], token);
var episode = fetched == default ? default : NoteParser.Parse(JsonNode.Parse(fetched.Root.GetRawText()));
if (episode is not { Type: "PodcastEpisode" } || episode.AttributedTo != author.ActorURI)
return;
post.Media = episode.Attachments.ToList();
post.Audio = episode.Audio;
post.Title ??= episode.Title;
post.CoverURL ??= episode.CoverURL;
post.Excerpt ??= ObjectShapes.Text(episode.ContentHtml, 1000);
}
public async Task<PostEntity> Parent(string inReplyTo, int depth, CancellationToken token)