Add chapter source fields (#6)

This commit is contained in:
Daniel J. Summers 2024-02-05 22:28:07 -05:00
parent f25426db5c
commit d378f690e4
5 changed files with 127 additions and 11 deletions

View File

@ -648,6 +648,9 @@ type EditPostModel = {
/// The explicit rating for this episode (optional, defaults to podcast setting) /// The explicit rating for this episode (optional, defaults to podcast setting)
Explicit: string Explicit: string
/// The chapter source ("internal" for chapters defined here, "external" for a file link, "none" if none defined)
ChapterSource: string
/// The URL for the chapter file for the episode (may be permalink; optional) /// The URL for the chapter file for the episode (may be permalink; optional)
ChapterFile: string ChapterFile: string
@ -713,6 +716,9 @@ type EditPostModel = {
ImageUrl = defaultArg episode.ImageUrl "" ImageUrl = defaultArg episode.ImageUrl ""
Subtitle = defaultArg episode.Subtitle "" Subtitle = defaultArg episode.Subtitle ""
Explicit = defaultArg (episode.Explicit |> Option.map string) "" Explicit = defaultArg (episode.Explicit |> Option.map string) ""
ChapterSource = if Option.isSome episode.Chapters then "internal"
elif Option.isSome episode.ChapterFile then "external"
else "none"
ChapterFile = defaultArg episode.ChapterFile "" ChapterFile = defaultArg episode.ChapterFile ""
ChapterType = defaultArg episode.ChapterType "" ChapterType = defaultArg episode.ChapterType ""
ContainsWaypoints = defaultArg episode.ChapterWaypoints false ContainsWaypoints = defaultArg episode.ChapterWaypoints false
@ -773,10 +779,19 @@ type EditPostModel = {
ImageUrl = noneIfBlank this.ImageUrl ImageUrl = noneIfBlank this.ImageUrl
Subtitle = noneIfBlank this.Subtitle Subtitle = noneIfBlank this.Subtitle
Explicit = noneIfBlank this.Explicit |> Option.map ExplicitRating.Parse Explicit = noneIfBlank this.Explicit |> Option.map ExplicitRating.Parse
Chapters = match post.Episode with Some e -> e.Chapters | None -> None Chapters = if this.ChapterSource = "internal" then
ChapterFile = noneIfBlank this.ChapterFile match post.Episode with
ChapterType = noneIfBlank this.ChapterType | Some e when Option.isSome e.Chapters -> e.Chapters
ChapterWaypoints = if this.ContainsWaypoints then Some true else None | Some _
| None -> Some []
else None
ChapterFile = if this.ChapterSource = "external" then noneIfBlank this.ChapterFile
else None
ChapterType = if this.ChapterSource = "external" then noneIfBlank this.ChapterType
else None
ChapterWaypoints = if this.ChapterSource = "none" then None
elif this.ContainsWaypoints then Some true
else None
TranscriptUrl = noneIfBlank this.TranscriptUrl TranscriptUrl = noneIfBlank this.TranscriptUrl
TranscriptType = noneIfBlank this.TranscriptType TranscriptType = noneIfBlank this.TranscriptType
TranscriptLang = noneIfBlank this.TranscriptLang TranscriptLang = noneIfBlank this.TranscriptLang

View File

@ -621,7 +621,7 @@ let editPostModelTests = testList "EditPostModel" [
ImageUrl = Some "uploads/podcast-cover.jpg" ImageUrl = Some "uploads/podcast-cover.jpg"
Subtitle = Some "Narration" Subtitle = Some "Narration"
Explicit = Some Clean Explicit = Some Clean
Chapters = None // for future implementation Chapters = None
ChapterFile = Some "uploads/1970/01/chapters.txt" ChapterFile = Some "uploads/1970/01/chapters.txt"
ChapterType = Some "chapters" ChapterType = Some "chapters"
ChapterWaypoints = Some true ChapterWaypoints = Some true
@ -661,6 +661,7 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal model.ImageUrl "" "ImageUrl not filled properly" Expect.equal model.ImageUrl "" "ImageUrl not filled properly"
Expect.equal model.Subtitle "" "Subtitle not filled properly" Expect.equal model.Subtitle "" "Subtitle not filled properly"
Expect.equal model.Explicit "" "Explicit not filled properly" Expect.equal model.Explicit "" "Explicit not filled properly"
Expect.equal model.ChapterSource "none" "ChapterSource not filled properly"
Expect.equal model.ChapterFile "" "ChapterFile not filled properly" Expect.equal model.ChapterFile "" "ChapterFile not filled properly"
Expect.equal model.ChapterType "" "ChapterType not filled properly" Expect.equal model.ChapterType "" "ChapterType not filled properly"
Expect.isFalse model.ContainsWaypoints "ContainsWaypoints should not have been set" Expect.isFalse model.ContainsWaypoints "ContainsWaypoints should not have been set"
@ -673,7 +674,7 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal model.EpisodeNumber "" "EpisodeNumber not filled properly" Expect.equal model.EpisodeNumber "" "EpisodeNumber not filled properly"
Expect.equal model.EpisodeDescription "" "EpisodeDescription not filled properly" Expect.equal model.EpisodeDescription "" "EpisodeDescription not filled properly"
} }
test "succeeds for full post" { test "succeeds for full post with external chapters" {
let model = EditPostModel.FromPost { WebLog.Empty with TimeZone = "Etc/GMT+1" } fullPost let model = EditPostModel.FromPost { WebLog.Empty with TimeZone = "Etc/GMT+1" } fullPost
Expect.equal model.PostId "a-post" "PostId not filled properly" Expect.equal model.PostId "a-post" "PostId not filled properly"
Expect.equal model.Title "A Post" "Title not filled properly" Expect.equal model.Title "A Post" "Title not filled properly"
@ -704,6 +705,7 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal model.ImageUrl "uploads/podcast-cover.jpg" "ImageUrl not filled properly" Expect.equal model.ImageUrl "uploads/podcast-cover.jpg" "ImageUrl not filled properly"
Expect.equal model.Subtitle "Narration" "Subtitle not filled properly" Expect.equal model.Subtitle "Narration" "Subtitle not filled properly"
Expect.equal model.Explicit "clean" "Explicit not filled properly" Expect.equal model.Explicit "clean" "Explicit not filled properly"
Expect.equal model.ChapterSource "external" "ChapterSource not filled properly"
Expect.equal model.ChapterFile "uploads/1970/01/chapters.txt" "ChapterFile not filled properly" Expect.equal model.ChapterFile "uploads/1970/01/chapters.txt" "ChapterFile not filled properly"
Expect.equal model.ChapterType "chapters" "ChapterType not filled properly" Expect.equal model.ChapterType "chapters" "ChapterType not filled properly"
Expect.isTrue model.ContainsWaypoints "ContainsWaypoints should have been set" Expect.isTrue model.ContainsWaypoints "ContainsWaypoints should have been set"
@ -716,6 +718,19 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal model.EpisodeNumber "322" "EpisodeNumber not filled properly" Expect.equal model.EpisodeNumber "322" "EpisodeNumber not filled properly"
Expect.equal model.EpisodeDescription "Episode 322" "EpisodeDescription not filled properly" Expect.equal model.EpisodeDescription "Episode 322" "EpisodeDescription not filled properly"
} }
test "succeeds for full post with internal chapters" {
let model =
EditPostModel.FromPost
{ WebLog.Empty with TimeZone = "Etc/GMT+1" }
{ fullPost with
Episode =
Some
{ fullPost.Episode.Value with
Chapters = Some []
ChapterFile = None
ChapterType = None } }
Expect.equal model.ChapterSource "internal" "ChapterSource not filled properly"
}
] ]
testList "IsNew" [ testList "IsNew" [
test "succeeds for a new post" { test "succeeds for a new post" {
@ -747,6 +762,7 @@ let editPostModelTests = testList "EditPostModel" [
ImageUrl = "updated-cover.png" ImageUrl = "updated-cover.png"
Subtitle = "Talking" Subtitle = "Talking"
Explicit = "no" Explicit = "no"
ChapterSource = "external"
ChapterFile = "updated-chapters.txt" ChapterFile = "updated-chapters.txt"
ChapterType = "indexes" ChapterType = "indexes"
TranscriptUrl = "updated-transcript.txt" TranscriptUrl = "updated-transcript.txt"
@ -787,6 +803,7 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal ep.ImageUrl (Some "updated-cover.png") "ImageUrl not filled properly" Expect.equal ep.ImageUrl (Some "updated-cover.png") "ImageUrl not filled properly"
Expect.equal ep.Subtitle (Some "Talking") "Subtitle not filled properly" Expect.equal ep.Subtitle (Some "Talking") "Subtitle not filled properly"
Expect.equal ep.Explicit (Some No) "ExplicitRating not filled properly" Expect.equal ep.Explicit (Some No) "ExplicitRating not filled properly"
Expect.isNone ep.Chapters "Chapters should have had no value"
Expect.equal ep.ChapterFile (Some "updated-chapters.txt") "ChapterFile not filled properly" Expect.equal ep.ChapterFile (Some "updated-chapters.txt") "ChapterFile not filled properly"
Expect.equal ep.ChapterType (Some "indexes") "ChapterType not filled properly" Expect.equal ep.ChapterType (Some "indexes") "ChapterType not filled properly"
Expect.equal ep.ChapterWaypoints (Some true) "ChapterWaypoints should have been set" Expect.equal ep.ChapterWaypoints (Some true) "ChapterWaypoints should have been set"
@ -840,6 +857,32 @@ let editPostModelTests = testList "EditPostModel" [
Expect.isNone ep.EpisodeNumber "EpisodeNumber not filled properly" Expect.isNone ep.EpisodeNumber "EpisodeNumber not filled properly"
Expect.isNone ep.EpisodeDescription "EpisodeDescription not filled properly" Expect.isNone ep.EpisodeDescription "EpisodeDescription not filled properly"
} }
test "succeeds for a podcast episode with internal chapters" {
let minModel =
{ updatedModel with
ChapterSource = "internal"
ChapterFile = ""
ChapterType = "" }
let post = minModel.UpdatePost fullPost (Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value
Expect.equal ep.Chapters (Some []) "Chapters not filled properly"
Expect.isNone ep.ChapterFile "ChapterFile not filled properly"
Expect.isNone ep.ChapterType "ChapterType not filled properly"
}
test "succeeds for a podcast episode with no chapters" {
let minModel = { updatedModel with ChapterSource = "none" }
let post =
minModel.UpdatePost
{ fullPost with Episode = Some { fullPost.Episode.Value with Chapters = Some [] } }
(Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value
Expect.isNone ep.Chapters "Chapters not filled properly"
Expect.isNone ep.ChapterFile "ChapterFile not filled properly"
Expect.isNone ep.ChapterType "ChapterType not filled properly"
Expect.isNone ep.ChapterWaypoints "ChapterWaypoints not filled properly"
}
test "succeeds for no podcast episode and no template" { test "succeeds for no podcast episode and no template" {
let post = { updatedModel with IsEpisode = false; Template = "" }.UpdatePost fullPost Noda.epoch let post = { updatedModel with IsEpisode = false; Template = "" }.UpdatePost fullPost Noda.epoch
Expect.isNone post.Template "Template not filled properly" Expect.isNone post.Template "Template not filled properly"

View File

@ -13,6 +13,12 @@
<a href="{{ entity_url_base | append: "/permalinks" | relative_link }}">Manage Permalinks</a> <a href="{{ entity_url_base | append: "/permalinks" | relative_link }}">Manage Permalinks</a>
<span class=text-muted> &bull; </span> <span class=text-muted> &bull; </span>
<a href="{{ entity_url_base | append: "/revisions" | relative_link }}">Manage Revisions</a> <a href="{{ entity_url_base | append: "/revisions" | relative_link }}">Manage Revisions</a>
{% if model.chapter_source == "internal" %}
<span id="chapterEditLink">
<span class=text-muted> &bull; </span>
<a href="{{ entity_url_base | append: "/chapters" | relative_link }}">Manage Chapters</a>
</span>
{% endif %}
</span> </span>
{%- endunless -%} {%- endunless -%}
</div> </div>

View File

@ -107,13 +107,43 @@
</div> </div>
</div> </div>
</div> </div>
<div class=row>
<div class="col-12 col-md-8 pb-3">
<div class="form-text">Chapters</div>
<div class="form-check form-check-inline">
<input type=radio name=ChapterSource id=chapterSourceNone value=none
class=form-check-input{% if model.chapter_source == "none" %} checked{% endif %}
onclick="Admin.setChapterSource('none')">
<label for=chapterSourceNone>None</label>
</div>
<div class="form-check form-check-inline">
<input type=radio name=ChapterSource id=chapterSourceInternal value=internal
class=form-check-input{% if model.chapter_source == "internal" %} checked{% endif %}
onclick="Admin.setChapterSource('internal')">
<label for=chapterSourceInternal>Defined Here</label>
</div>
<div class="form-check form-check-inline">
<input type=radio name=ChapterSource id=chapterSourceExternal value=none
class=form-check-input{% if model.chapter_source == "external" %} checked{% endif %}
onclick="Admin.setChapterSource('external')">
<label for=chapterSourceExternal>Separate File</label>
</div>
</div>
<div class="col-md-4 d-flex justify-content-center">
<div class="form-check form-switch align-self-center pb-3">
<input type=checkbox name=ContainsWaypoints id=containsWaypoints class=form-check-input
value=true{% if model.contains_waypoints %} checked{% endif %}>
<label for=containsWaypoints>Chapters contain waypoints</label>
</div>
</div>
</div>
<div class=row> <div class=row>
<div class="col-12 col-md-8 pb-3"> <div class="col-12 col-md-8 pb-3">
<div class=form-floating> <div class=form-floating>
<input type=text name=ChapterFile id=chapterFile class=form-control placeholder="Chapter File" <input type=text name=ChapterFile id=chapterFile class=form-control placeholder="Chapter File"
value="{{ model.chapter_file }}"> value="{{ model.chapter_file }}">
<label for=chapterFile>Chapter File</label> <label for=chapterFile>Chapter File</label>
<div class=form-text>Optional; relative URL served from this web log</div> <div class=form-text>Relative URL served from this web log</div>
</div> </div>
</div> </div>
<div class="col-12 col-md-4 pb-3"> <div class="col-12 col-md-4 pb-3">

View File

@ -212,15 +212,37 @@ this.Admin = {
this.nextPermalink++ this.nextPermalink++
}, },
/**
* Set the chapter type for a podcast episode
* @param {"none"|"internal"|"external"} src The source for chapters for this episode
*/
setChapterSource(src) {
document.getElementById("containsWaypoints").disabled = src === "none"
const isDisabled = src === "none" || src === "internal"
const chapterFile = document.getElementById("chapterFile")
chapterFile.disabled = isDisabled
chapterFile.required = !isDisabled
document.getElementById("chapterType").disabled = isDisabled
const link = document.getElementById("chapterEditLink")
if (link) link.style.display = src === "none" || src === "external" ? "none" : ""
},
/** /**
* Enable or disable podcast fields * Enable or disable podcast fields
*/ */
toggleEpisodeFields() { toggleEpisodeFields() {
const disabled = !document.getElementById("isEpisode").checked const disabled = !document.getElementById("isEpisode").checked
;[ "media", "mediaType", "length", "duration", "subtitle", "imageUrl", "explicit", "chapterFile", "chapterType", let fields = [
"transcriptUrl", "transcriptType", "transcriptLang", "transcriptCaptions", "seasonNumber", "seasonDescription", "media", "mediaType", "length", "duration", "subtitle", "imageUrl", "explicit", "transcriptUrl", "transcriptType",
"episodeNumber", "episodeDescription" "transcriptLang", "transcriptCaptions", "seasonNumber", "seasonDescription", "episodeNumber", "episodeDescription"
].forEach(it => document.getElementById(it).disabled = disabled) ]
if (disabled) {
fields.push("chapterFile", "chapterType", "containsWaypoints")
} else {
const src = [...document.getElementsByName("ChapterSource")].filter(it => it.checked)[0].value
this.setChapterSource(src)
}
fields.forEach(it => document.getElementById(it).disabled = disabled)
}, },
/** /**