From 16603bbcaf16b22651337716f57987c01a717c2c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 27 Jun 2022 20:34:30 -0400 Subject: [PATCH] Render feed from episode (#9) - Render chapter if present (#5) - Render transcript if present (#8) - Require transcript type if URL entered (#8) --- src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs | 32 +++--- src/MyWebLog/Handlers/Feed.fs | 101 +++++++++++++------ src/admin-theme/post-edit.liquid | 6 +- src/admin-theme/wwwroot/admin.js | 7 ++ 4 files changed, 99 insertions(+), 47 deletions(-) diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs index 6d7b848..2418721 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs @@ -233,22 +233,24 @@ type SQLiteWebLogData (conn : SqliteConnection) = let postSubQuery = subQuery "post" let pageSubQuery = subQuery "page" cmd.CommandText <- - $"""DELETE FROM post_comment WHERE post_id IN {postSubQuery}; - DELETE FROM post_revision WHERE post_id IN {postSubQuery}; - DELETE FROM post_episode WHERE post_id IN {postSubQuery}; - DELETE FROM post_tag WHERE post_id IN {postSubQuery}; - DELETE FROM post_category WHERE post_id IN {postSubQuery}; - DELETE FROM post_meta WHERE post_id IN {postSubQuery}; - DELETE FROM post WHERE web_log_id = @webLogId; - DELETE FROM page_revision WHERE page_id IN {pageSubQuery}; - DELETE FROM page_meta WHERE page_id IN {pageSubQuery}; - DELETE FROM page WHERE web_log_id = @webLogId; - DELETE FROM category WHERE web_log_id = @webLogId; - DELETE FROM tag_map WHERE web_log_id = @webLogId; - DELETE FROM web_log_user WHERE web_log_id = @webLogId; + $"""DELETE FROM post_comment WHERE post_id IN {postSubQuery}; + DELETE FROM post_revision WHERE post_id IN {postSubQuery}; + DELETE FROM post_permalink WHERE post_id IN {postSubQuery}; + DELETE FROM post_episode WHERE post_id IN {postSubQuery}; + DELETE FROM post_tag WHERE post_id IN {postSubQuery}; + DELETE FROM post_category WHERE post_id IN {postSubQuery}; + DELETE FROM post_meta WHERE post_id IN {postSubQuery}; + DELETE FROM post WHERE web_log_id = @webLogId; + DELETE FROM page_revision WHERE page_id IN {pageSubQuery}; + DELETE FROM page_permalink WHERE page_id IN {pageSubQuery}; + DELETE FROM page_meta WHERE page_id IN {pageSubQuery}; + DELETE FROM page WHERE web_log_id = @webLogId; + DELETE FROM category WHERE web_log_id = @webLogId; + DELETE FROM tag_map WHERE web_log_id = @webLogId; + DELETE FROM web_log_user WHERE web_log_id = @webLogId; DELETE FROM web_log_feed_podcast WHERE feed_id IN {subQuery "web_log_feed"}; - DELETE FROM web_log_feed WHERE web_log_id = @webLogId; - DELETE FROM web_log WHERE id = @webLogId""" + DELETE FROM web_log_feed WHERE web_log_id = @webLogId; + DELETE FROM web_log WHERE id = @webLogId""" do! write cmd } diff --git a/src/MyWebLog/Handlers/Feed.fs b/src/MyWebLog/Handlers/Feed.fs index 4745ed2..d5c7964 100644 --- a/src/MyWebLog/Handlers/Feed.fs +++ b/src/MyWebLog/Handlers/Feed.fs @@ -76,6 +76,9 @@ module private Namespace = /// iTunes elements let iTunes = "http://www.itunes.com/dtds/podcast-1.0.dtd" + /// Podcast Index (AKA "podcasting 2.0") + let podcast = "https://podcastindex.org/namespace/1.0" + /// Enables chapters let psc = "http://podlove.org/simple-chapters/" @@ -127,36 +130,23 @@ let private toFeedItem webLog (authors : MetaItem list) (cats : DisplayCategory[ item /// Add episode information to a podcast feed item -let private addEpisode webLog (feed : CustomFeed) (post : Post) (item : SyndicationItem) = - let podcast = Option.get feed.podcast - let meta name = post.metadata |> List.tryFind (fun it -> it.name = name) - let value (item : MetaItem) = item.value +let private addEpisode webLog (podcast : PodcastOptions) (episode : Episode) (post : Post) (item : SyndicationItem) = + // Convert non-absolute URLs to an absolute URL for this web log + let toAbsolute (link : string) = if link.StartsWith "http" then link else WebLog.absoluteUrl webLog (Permalink link) let epMediaUrl = - match (meta >> Option.get >> value) "episode_media_file" with + match episode.media with | link when link.StartsWith "http" -> link | link when Option.isSome podcast.mediaBaseUrl -> $"{podcast.mediaBaseUrl.Value}{link}" | link -> WebLog.absoluteUrl webLog (Permalink link) - let epMediaType = - match meta "episode_media_type", podcast.defaultMediaType with - | Some epType, _ -> Some epType.value - | None, Some defType -> Some defType - | _ -> None - let epImageUrl = - match defaultArg ((meta >> Option.map value) "episode_image") (Permalink.toString podcast.imageUrl) with - | link when link.StartsWith "http" -> link - | link -> WebLog.absoluteUrl webLog (Permalink link) - let epExplicit = - try - (meta >> Option.map (value >> ExplicitRating.parse)) "episode_explicit" - |> Option.defaultValue podcast.explicit - |> ExplicitRating.toString - with :? ArgumentException -> ExplicitRating.toString podcast.explicit + let epMediaType = [ episode.mediaType; podcast.defaultMediaType ] |> List.tryFind Option.isSome |> Option.flatten + let epImageUrl = defaultArg episode.imageUrl (Permalink.toString podcast.imageUrl) |> toAbsolute + let epExplicit = defaultArg episode.explicit podcast.explicit |> ExplicitRating.toString let xmlDoc = XmlDocument () let enclosure = let it = xmlDoc.CreateElement "enclosure" it.SetAttribute ("url", epMediaUrl) - meta "episode_media_length" |> Option.iter (fun len -> it.SetAttribute ("length", len.value)) + it.SetAttribute ("length", string episode.length) epMediaType |> Option.iter (fun typ -> it.SetAttribute ("type", typ)) it let image = @@ -169,10 +159,57 @@ let private addEpisode webLog (feed : CustomFeed) (post : Post) (item : Syndicat item.ElementExtensions.Add ("creator", Namespace.dc, podcast.displayedAuthor) item.ElementExtensions.Add ("author", Namespace.iTunes, podcast.displayedAuthor) item.ElementExtensions.Add ("explicit", Namespace.iTunes, epExplicit) - meta "episode_subtitle" - |> Option.iter (fun it -> item.ElementExtensions.Add ("subtitle", Namespace.iTunes, it.value)) - meta "episode_duration" - |> Option.iter (fun it -> item.ElementExtensions.Add ("duration", Namespace.iTunes, it.value)) + episode.subtitle |> Option.iter (fun it -> item.ElementExtensions.Add ("subtitle", Namespace.iTunes, it)) + episode.duration + |> Option.iter (fun it -> item.ElementExtensions.Add ("duration", Namespace.iTunes, it.ToString """hh\:mm\:ss""")) + + match episode.chapterFile with + | Some chapters -> + let url = toAbsolute chapters + let typ = + match episode.chapterType with + | Some mime -> Some mime + | None when chapters.EndsWith ".json" -> Some "application/json+chapters" + | None -> None + let elt = xmlDoc.CreateElement ("podcast", "chapters", Namespace.podcast) + elt.SetAttribute ("url", url) + typ |> Option.iter (fun it -> elt.SetAttribute ("type", it)) + item.ElementExtensions.Add elt + | None -> () + + match episode.transcriptUrl with + | Some transcript -> + let url = toAbsolute transcript + let elt = xmlDoc.CreateElement ("podcast", "transcript", Namespace.podcast) + elt.SetAttribute ("url", url) + elt.SetAttribute ("type", Option.get episode.transcriptType) + episode.transcriptLang |> Option.iter (fun it -> elt.SetAttribute ("language", it)) + if defaultArg episode.transcriptCaptions false then + elt.SetAttribute ("rel", "captions") + item.ElementExtensions.Add elt + | None -> () + + match episode.seasonNumber with + | Some season -> + match episode.seasonDescription with + | Some desc -> + let elt = xmlDoc.CreateElement ("podcast", "season", Namespace.podcast) + elt.SetAttribute ("name", desc) + elt.InnerText <- string season + item.ElementExtensions.Add elt + | None -> item.ElementExtensions.Add ("season", Namespace.podcast, string season) + | None -> () + + match episode.episodeNumber with + | Some epNumber -> + match episode.episodeDescription with + | Some desc -> + let elt = xmlDoc.CreateElement ("podcast", "episode", Namespace.podcast) + elt.SetAttribute ("name", desc) + elt.InnerText <- string epNumber + item.ElementExtensions.Add elt + | None -> item.ElementExtensions.Add ("episode", Namespace.podcast, string epNumber) + | None -> () if post.metadata |> List.exists (fun it -> it.name = "chapter") then try @@ -216,7 +253,12 @@ let private addPodcast webLog (rssFeed : SyndicationFeed) (feed : CustomFeed) = let xmlDoc = XmlDocument () - [ "dc", Namespace.dc; "itunes", Namespace.iTunes; "psc", Namespace.psc; "rawvoice", Namespace.rawVoice ] + [ "dc", Namespace.dc + "itunes", Namespace.iTunes + "podcast", Namespace.podcast + "psc", Namespace.psc + "rawvoice", Namespace.rawVoice + ] |> List.iter (fun (alias, nsUrl) -> addNamespace rssFeed alias nsUrl) let categorization = @@ -314,10 +356,9 @@ let createFeed (feedType : FeedType) posts : HttpHandler = fun next ctx -> backg let toItem post = let item = toFeedItem webLog authors cats tagMaps post - match podcast with - | Some feed when post.metadata |> List.exists (fun it -> it.name = "episode_media_file") -> - addEpisode webLog feed post item - | Some _ -> + match podcast, post.episode with + | Some feed, Some episode -> addEpisode webLog (Option.get feed.podcast) episode post item + | Some _, _ -> warn "Feed" ctx $"[{webLog.name} {Permalink.toString self}] \"{stripHtml post.title}\" has no media" item | _ -> item diff --git a/src/admin-theme/post-edit.liquid b/src/admin-theme/post-edit.liquid index b670ae1..de9cc7c 100644 --- a/src/admin-theme/post-edit.liquid +++ b/src/admin-theme/post-edit.liquid @@ -157,7 +157,8 @@
+ placeholder="Transcript URL" value="{{ model.transcript_url }}" + onkeyup="Admin.requireTranscriptType()">
Optional; relative URL served from this web log
@@ -165,7 +166,8 @@
+ placeholder="Transcript Type" value="{{ model.transcript_type }}" + {%- if model.transcript_url != "" %} required{% endif %}>
Recommended if transcript file provided
diff --git a/src/admin-theme/wwwroot/admin.js b/src/admin-theme/wwwroot/admin.js index 1e36d4f..200b6fb 100644 --- a/src/admin-theme/wwwroot/admin.js +++ b/src/admin-theme/wwwroot/admin.js @@ -255,6 +255,13 @@ document.getElementById(`link_${idx}`).remove() }, + /** + * Require transcript type if transcript URL is present + */ + requireTranscriptType() { + document.getElementById("transcriptType").required = document.getElementById("transcriptUrl").value.trim() !== "" + }, + /** * Show messages that may have come with an htmx response * @param messages The messages from the response