WIP on episode meta fields

This commit is contained in:
Daniel J. Summers 2022-06-02 21:49:53 -04:00
parent de92761b04
commit 7a1d438d68
7 changed files with 137 additions and 35 deletions

View File

@ -610,7 +610,7 @@ type PostListItem =
tags : string list tags : string list
/// Metadata for the post /// Metadata for the post
meta : MetaItem list metadata : MetaItem list
} }
/// Create a post list item from a post /// Create a post list item from a post
@ -627,7 +627,7 @@ type PostListItem =
text = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/") text = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/")
categoryIds = post.categoryIds |> List.map CategoryId.toString categoryIds = post.categoryIds |> List.map CategoryId.toString
tags = post.tags tags = post.tags
meta = post.metadata metadata = post.metadata
} }

View File

@ -122,21 +122,21 @@ let private addEpisode webLog (feed : CustomFeed) (post : Post) (item : Syndicat
let meta name = post.metadata |> List.tryFind (fun it -> it.name = name) let meta name = post.metadata |> List.tryFind (fun it -> it.name = name)
let value (item : MetaItem) = item.value let value (item : MetaItem) = item.value
let epMediaUrl = let epMediaUrl =
match (meta >> Option.get >> value) "media" with match (meta >> Option.get >> value) "episode_media_file" with
| link when link.StartsWith "http" -> link | link when link.StartsWith "http" -> link
| link -> WebLog.absoluteUrl webLog (Permalink link) | link -> WebLog.absoluteUrl webLog (Permalink link)
let epMediaType = let epMediaType =
match meta "mediaType", podcast.defaultMediaType with match meta "episode_media_type", podcast.defaultMediaType with
| Some epType, _ -> Some epType.value | Some epType, _ -> Some epType.value
| None, Some defType -> Some defType | None, Some defType -> Some defType
| _ -> None | _ -> None
let epImageUrl = let epImageUrl =
match defaultArg ((meta >> Option.map value) "image") (Permalink.toString podcast.imageUrl) with match defaultArg ((meta >> Option.map value) "episode_image") (Permalink.toString podcast.imageUrl) with
| link when link.StartsWith "http" -> link | link when link.StartsWith "http" -> link
| link -> WebLog.absoluteUrl webLog (Permalink link) | link -> WebLog.absoluteUrl webLog (Permalink link)
let epExplicit = let epExplicit =
try try
(meta >> Option.map (value >> ExplicitRating.parse)) "explicit" (meta >> Option.map (value >> ExplicitRating.parse)) "episode_explicit"
|> Option.defaultValue podcast.explicit |> Option.defaultValue podcast.explicit
|> ExplicitRating.toString |> ExplicitRating.toString
with :? ArgumentException -> ExplicitRating.toString podcast.explicit with :? ArgumentException -> ExplicitRating.toString podcast.explicit
@ -144,7 +144,7 @@ let private addEpisode webLog (feed : CustomFeed) (post : Post) (item : Syndicat
let xmlDoc = XmlDocument () let xmlDoc = XmlDocument ()
let enclosure = xmlDoc.CreateElement "enclosure" let enclosure = xmlDoc.CreateElement "enclosure"
enclosure.SetAttribute ("url", epMediaUrl) enclosure.SetAttribute ("url", epMediaUrl)
meta "length" |> Option.iter (fun it -> enclosure.SetAttribute ("length", it.value)) meta "episode_media_length" |> Option.iter (fun it -> enclosure.SetAttribute ("length", it.value))
epMediaType |> Option.iter (fun typ -> enclosure.SetAttribute ("type", typ)) epMediaType |> Option.iter (fun typ -> enclosure.SetAttribute ("type", typ))
item.ElementExtensions.Add enclosure item.ElementExtensions.Add enclosure
@ -153,8 +153,10 @@ let private addEpisode webLog (feed : CustomFeed) (post : Post) (item : Syndicat
item.ElementExtensions.Add ("summary", Namespace.iTunes, stripHtml post.text) item.ElementExtensions.Add ("summary", Namespace.iTunes, stripHtml post.text)
item.ElementExtensions.Add ("image", Namespace.iTunes, epImageUrl) item.ElementExtensions.Add ("image", Namespace.iTunes, epImageUrl)
item.ElementExtensions.Add ("explicit", Namespace.iTunes, epExplicit) item.ElementExtensions.Add ("explicit", Namespace.iTunes, epExplicit)
meta "subtitle" |> Option.iter (fun it -> item.ElementExtensions.Add ("subtitle", Namespace.iTunes, it.value)) meta "episode_subtitle"
meta "duration" |> Option.iter (fun it -> item.ElementExtensions.Add ("duration", Namespace.iTunes, it.value)) |> 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))
if post.metadata |> List.exists (fun it -> it.name = "chapter") then if post.metadata |> List.exists (fun it -> it.name = "chapter") then
try try

View File

@ -232,10 +232,13 @@ let edit postId : HttpHandler = fun next ctx -> task {
match result with match result with
| Some (title, post) -> | Some (title, post) ->
let! cats = Data.Category.findAllForView webLog.id conn let! cats = Data.Category.findAllForView webLog.id conn
let model = EditPostModel.fromPost webLog post
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
csrf = csrfToken ctx csrf = csrfToken ctx
model = EditPostModel.fromPost webLog post model = model
metadata = Array.zip model.metaNames model.metaValues
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
page_title = title page_title = title
categories = cats categories = cats
|} |}

View File

@ -55,8 +55,14 @@
</button> </button>
</legend> </legend>
<div id="metaItemContainer" class="collapse"> <div id="metaItemContainer" class="collapse">
{%- assign has_media = model.metaNames | where: "episode_media_file" | size -%}
{%- unless has_media == 0 %}
<button class="btn btn-sm btn-secondary" onclick="Admin.addPodcastFields()" id="addPodcastFieldButton">
Add Podcast Episode Fields
</button>
{%- endunless %}
<div id="metaItems" class="container"> <div id="metaItems" class="container">
{%- for meta in model.metadata %} {%- for meta in metadata %}
<div id="meta_{{ meta[0] }}" class="row mb-3"> <div id="meta_{{ meta[0] }}" class="row mb-3">
<div class="col-1 text-center align-self-center"> <div class="col-1 text-center align-self-center">
<button type="button" class="btn btn-sm btn-danger" onclick="Admin.removeMetaItem({{ meta[0] }})"> <button type="button" class="btn btn-sm btn-danger" onclick="Admin.removeMetaItem({{ meta[0] }})">
@ -82,7 +88,7 @@
</div> </div>
<button type="button" class="btn btn-sm btn-secondary" onclick="Admin.addMetaItem()">Add an Item</button> <button type="button" class="btn btn-sm btn-secondary" onclick="Admin.addMetaItem()">Add an Item</button>
<script> <script>
document.addEventListener("DOMContentLoaded", () => Admin.setNextMetaIndex({{ model.metadata | size }})) document.addEventListener("DOMContentLoaded", () => Admin.setNextMetaIndex({{ metadata | size }}))
</script> </script>
</div> </div>
</fieldset> </fieldset>

View File

@ -33,8 +33,8 @@
#[i.fa.fa-clock-o(title='Clock' aria-hidden='true')] #[= readingTime(post.content, 'minutes', 175)] #[i.fa.fa-clock-o(title='Clock' aria-hidden='true')] #[= readingTime(post.content, 'minutes', 175)]
{% endcomment %} {% endcomment %}
</p> </p>
{%- assign media = post.meta | value: "media" -%} {%- assign media = post.metadata | value: "episode_media_file" -%}
{%- unless media == "-- media not found --" %} {%- unless media == "-- episode_media_file not found --" %}
<aside class="podcast"> <aside class="podcast">
<p class="text-center"><strong>Listen While<br>You Read</strong></p> <p class="text-center"><strong>Listen While<br>You Read</strong></p>
<audio controls onplaying="awftw.countPlay('{{ media }}')"> <audio controls onplaying="awftw.countPlay('{{ media }}')">

View File

@ -2,8 +2,8 @@
<div class="content"> <div class="content">
<article class="item"> <article class="item">
<h1 class="item-heading">{{ post.title }}</h1> <h1 class="item-heading">{{ post.title }}</h1>
{% assign media = post.meta | value: "media" %} {% assign media = post.meta | value: "episode_media_file" %}
{%- unless media == "-- media not found --" %} {%- unless media == "-- episode_media_file not found --" %}
<aside class="podcast"> <aside class="podcast">
<p class="text-center"><strong>Listen While<br>You Read</strong></p> <p class="text-center"><strong>Listen While<br>You Read</strong></p>
<audio controls onplaying="awftw.countPlay('{{ media }}')"> <audio controls onplaying="awftw.countPlay('{{ media }}')">

View File

@ -22,10 +22,10 @@
}, },
/** /**
* Add a new row for metadata entry * Create a metadata remove button
* @returns {HTMLDivElement} The column with the remove button
*/ */
addMetaItem() { createMetaRemoveColumn() {
// Remove button
const removeBtn = document.createElement("button") const removeBtn = document.createElement("button")
removeBtn.type = "button" removeBtn.type = "button"
removeBtn.className = "btn btn-sm btn-danger" removeBtn.className = "btn btn-sm btn-danger"
@ -36,49 +36,100 @@
removeCol.className = "col-1 text-center align-self-center" removeCol.className = "col-1 text-center align-self-center"
removeCol.appendChild(removeBtn) removeCol.appendChild(removeBtn)
// Name return removeCol
},
/**
* Create a metadata name field
* @returns {HTMLInputElement} The name input element
*/
createMetaNameField() {
const nameField = document.createElement("input") const nameField = document.createElement("input")
nameField.type = "text" nameField.type = "text"
nameField.name = "metaNames" nameField.name = "metaNames"
nameField.id = `metaNames_${this.nextMetaIndex}` nameField.id = `metaNames_${this.nextMetaIndex}`
nameField.className = "form-control" nameField.className = "form-control"
nameField.placeholder = "Name" nameField.placeholder = "Name"
return nameField
},
/**
* Create a metadata name column using the given name input field
* @param {HTMLInputElement} field The name field for the column
* @returns {HTMLDivElement} The name column
*/
createMetaNameColumn(field) {
const nameLabel = document.createElement("label") const nameLabel = document.createElement("label")
nameLabel.htmlFor = nameField.id nameLabel.htmlFor = field.id
nameLabel.innerText = nameField.placeholder nameLabel.innerText = field.placeholder
const nameFloat = document.createElement("div") const nameFloat = document.createElement("div")
nameFloat.className = "form-floating" nameFloat.className = "form-floating"
nameFloat.appendChild(nameField) nameFloat.appendChild(field)
nameFloat.appendChild(nameLabel) nameFloat.appendChild(nameLabel)
const nameCol = document.createElement("div") const nameCol = document.createElement("div")
nameCol.className = "col-3" nameCol.className = "col-3"
nameCol.appendChild(nameFloat) nameCol.appendChild(nameFloat)
// Value return nameCol
},
/**
* Create a metadata value field
* @returns {HTMLInputElement} The metadata value field
*/
createMetaValueField() {
const valueField = document.createElement("input") const valueField = document.createElement("input")
valueField.type = "text" valueField.type = "text"
valueField.name = "metaValues" valueField.name = "metaValues"
valueField.id = `metaValues_${this.nextMetaIndex}` valueField.id = `metaValues_${this.nextMetaIndex}`
valueField.className = "form-control" valueField.className = "form-control"
valueField.placeholder = "Value" valueField.placeholder = "Value"
return valueField
},
/**
* Create a metadata value column using the given input field
* @param {HTMLInputElement} field The metadata value input field
* @param {string|undefined} hintText Text to be added below the field
* @returns {HTMLDivElement} The value column
*/
createMetaValueColumn(field, hintText) {
const valueLabel = document.createElement("label") const valueLabel = document.createElement("label")
valueLabel.htmlFor = valueField.id valueLabel.htmlFor = field.id
valueLabel.innerText = valueField.placeholder valueLabel.innerText = field.placeholder
const valueFloat = document.createElement("div") const valueFloat = document.createElement("div")
valueFloat.className = "form-floating" valueFloat.className = "form-floating"
valueFloat.appendChild(valueField) valueFloat.appendChild(field)
valueFloat.appendChild(valueLabel) valueFloat.appendChild(valueLabel)
if (hintText) {
const valueHint = document.createElement("div")
valueHint.className = "form-text"
valueHint.innerText = hintText
valueFloat.appendChild(valueHint)
}
const valueCol = document.createElement("div") const valueCol = document.createElement("div")
valueCol.className = "col-8" valueCol.className = "col-8"
valueCol.appendChild(valueFloat) valueCol.appendChild(valueFloat)
// Put it all together return valueCol
},
/**
* Construct and add a metadata item row
* @param {HTMLDivElement} removeCol The column with the remove button
* @param {HTMLDivElement} nameCol The column with the name field
* @param {HTMLDivElement} valueCol The column with the value field
*/
createMetaRow(removeCol, nameCol, valueCol) {
const newRow = document.createElement("div") const newRow = document.createElement("div")
newRow.className = "row mb-3" newRow.className = "row mb-3"
newRow.id = `meta_${this.nextMetaIndex}` newRow.id = `meta_${this.nextMetaIndex}`
@ -87,10 +138,23 @@
newRow.appendChild(valueCol) newRow.appendChild(valueCol)
document.getElementById("metaItems").appendChild(newRow) document.getElementById("metaItems").appendChild(newRow)
document.getElementById(nameField.id).focus()
this.nextMetaIndex++ this.nextMetaIndex++
}, },
/**
* Add a new row for metadata entry
*/
addMetaItem() {
const nameField = this.createMetaNameField()
this.createMetaRow(
this.createMetaRemoveColumn(),
this.createMetaNameColumn(nameField),
this.createMetaValueColumn(this.createMetaValueField(), undefined))
document.getElementById(nameField.id).focus()
},
/** /**
* Add a new row for a permalink * Add a new row for a permalink
*/ */
@ -139,6 +203,33 @@
this.nextPermalink++ this.nextPermalink++
}, },
/**
* Add podcast episode metadata fields
*/
addPodcastFields() {
[ [ "episode_media_file", true, "required; relative URL will be appended to configured base media path" ],
[ "episode_media_length", true, "required; file size in bytes" ],
[ "episode_duration", false, "suggested; format is HH:MM:SS" ],
[ "episode_media_type", false, "optional; blank uses podcast default" ],
[ "episode_image", false, "optional; relative URLs are served from this web log" ],
[ "episode_subtitle", false, "optional" ],
[ "episode_explicit", false, "optional; blank uses podcast default"]
].forEach(([fieldName, isRequired, hintText]) => {
const nameField = this.createMetaNameField()
nameField.value = fieldName
nameField.readOnly = true
const valueField = this.createMetaValueField()
valueField.required = isRequired
this.createMetaRow(
this.createMetaRemoveColumn(),
this.createMetaNameColumn(nameField),
this.createMetaValueColumn(valueField, hintText))
})
document.getElementById("addPodcastFieldButton").remove()
},
/** /**
* Check to enable or disable podcast fields * Check to enable or disable podcast fields
*/ */