Move podcast types to own file; more WIP on role (#17)

This commit is contained in:
2026-08-02 14:02:47 -04:00
parent e7261f6019
commit be4f621d42
7 changed files with 1188 additions and 721 deletions
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<Compile Include="PodcastTypes.fs" />
<Compile Include="SupportTypes.fs" />
<Compile Include="DataTypes.fs" />
<Compile Include="ViewModels.fs" />
+531
View File
@@ -0,0 +1,531 @@
namespace MyWebLog
/// <summary>Valid values for the iTunes explicit rating</summary>
[<Struct>]
type ExplicitRating =
| Yes
| No
| Clean
/// <summary>Parse a string into an explicit rating</summary>
/// <param name="rating">The string representation of the rating</param>
/// <returns>The <c>ExplicitRating</c> parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse rating =
match rating with
| "yes" -> Yes
| "no" -> No
| "clean" -> Clean
| _ -> invalidArg (nameof rating) $"{rating} is not a valid explicit rating"
/// <inheritdoc />
override this.ToString() =
match this with Yes -> "yes" | No -> "no" | Clean -> "clean"
/// <summary>A location (specified by Podcast Index)</summary>
type Location = {
/// <summary>The name of the location (free-form text)</summary>
Name: string
/// <summary>A geographic coordinate string (RFC 5870)</summary>
Geo: string
/// <summary>An OpenStreetMap query</summary>
Osm: string option
}
open NodaTime
/// <summary>A chapter in a podcast episode</summary>
type Chapter = {
/// <summary>The start time for the chapter</summary>
StartTime: Duration
/// <summary>The title for this chapter</summary>
Title: string option
/// <summary>A URL for an image for this chapter</summary>
ImageUrl: string option
/// <summary>A URL with information pertaining to this chapter</summary>
Url: string option
/// <summary>Whether this chapter is hidden</summary>
IsHidden: bool option
/// <summary>The episode end time for the chapter</summary>
EndTime: Duration option
/// <summary>A location that applies to a chapter</summary>
Location: Location option
} with
/// <summary>An empty chapter</summary>
static member Empty =
{ StartTime = Duration.Zero
Title = None
ImageUrl = None
Url = None
IsHidden = None
EndTime = None
Location = None }
/// <summary>A group to which a podcast person can belong</summary>
[<Struct>]
type PersonGroup =
| Administration
| AudioPostProduction
| AudioProduction
| Cast
| Community
| CreativeDirection
| Misc
| VideoPostProduction
| VideoProduction
| Visuals
| Writing
/// <summary>Parse a string into a group</summary>
/// <param name="group">The string representation of the group</param>
/// <returns>The <c>PersonGroup</c> instance parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse group =
match group with
| "Administration" -> Administration
| "Audio Post-Production" -> AudioPostProduction
| "Audio Production" -> AudioProduction
| "Cast" -> Cast
| "Community" -> Community
| "Creative Direction" -> CreativeDirection
| "Misc." -> Misc
| "Video Post-Production" -> VideoPostProduction
| "Video Production" -> VideoProduction
| "Visuals" -> Visuals
| "Writing" -> Writing
| _ -> invalidArg (nameof group) $"{group} is not a valid group"
/// <inheritdoc />
override this.ToString() =
match this with
| Administration -> "Administration"
| AudioPostProduction -> "Audio Post-Production"
| AudioProduction -> "Audio Production"
| Cast -> "Cast"
| Community -> "Community"
| CreativeDirection -> "Creative Direction"
| Misc -> "Misc."
| VideoPostProduction -> "Video Post-Production"
| VideoProduction -> "Video Production"
| Visuals -> "Visuals"
| Writing -> "Writing"
/// <summary>The role a person plays for a podcast or a podcast episode</summary>
type PersonRole =
| Announcer
| AssistantCamera
| AssistantDirector
| AssistantEditor
| AssociateProducer
| AudioEditor
| AudioEngineer
| Author
| BookingCoordinator
| CameraGrip
| CameraOperator
| CoHost
| Composer
| Consultant
| ContentManager
| CoverArtDesigner
| CoWriter
| CreativeDirector
| DevelopmentProducer
| Director
| EditorialDirector
| ExecutiveProducer
| FactChecker
| FoleyArtist
| GraphicDesigner
| Guest
| GuestHost
| GuestWriter
| Host
| Intern
| LightingDesigner
| Logger
| ManagingEditor
| MarketingManager
| MusicContributor
| MusicProduction
| Narrator
| PostProductionEngineer
| Producer
| ProductionAssistant
| ProductionCoordinator
| RemoteRecordingEngineer
| Reporter
| Researcher
| SalesManager
| SalesRepresentative
| ScriptCoordinator
| ScriptEditor
| SeniorProducer
| SocialMediaManager
| Songwriter
| SoundDesigner
| StoryEditor
| StudioCoordinator
| TechnicalDirector
| TechnicalManager
| ThemeMusic
| Transcriber
| Translator
| VideoEditor
| VoiceActor
| Writer
| WritingEditor
/// <summary>Parse a string into a role</summary>
/// <param name="role">The string representation of the role</param>
/// <returns>The <c>PersonRole</c> instance parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse role =
match role with
| "Announcer" -> Announcer
| "Assistant Camera" -> AssistantCamera
| "Assistant Director" -> AssistantDirector
| "Assistant Editor" -> AssistantEditor
| "Associate Producer" -> AssociateProducer
| "Audio Editor" -> AudioEditor
| "Audio Engineer" -> AudioEngineer
| "Author" -> Author
| "Booking Coordinator" -> BookingCoordinator
| "Camera Grip" -> CameraGrip
| "Camera Operator" -> CameraOperator
| "Co-Host" -> CoHost
| "Composer" -> Composer
| "Consultant" -> Consultant
| "Content Manager" -> ContentManager
| "Cover Art Designer" -> CoverArtDesigner
| "Co-Writer" -> CoWriter
| "Creative Director" -> CreativeDirector
| "Development Producer" -> DevelopmentProducer
| "Director" -> Director
| "Editorial Director" -> EditorialDirector
| "Executive Producer" -> ExecutiveProducer
| "Fact Checker" -> FactChecker
| "Foley Artist" -> FoleyArtist
| "Graphic Designer" -> GraphicDesigner
| "Guest" -> Guest
| "Guest Host" -> GuestHost
| "Guest Writer" -> GuestWriter
| "Host" -> Host
| "Intern" -> Intern
| "Lighting Designer" -> LightingDesigner
| "Logger" -> Logger
| "Managing Editor" -> ManagingEditor
| "Marketing Manager" -> MarketingManager
| "Music Contributor" -> MusicContributor
| "Music Production" -> MusicProduction
| "Narrator" -> Narrator
| "Post Production Engineer" -> PostProductionEngineer
| "Producer" -> Producer
| "Production Assistant" -> ProductionAssistant
| "Production Coordinator" -> ProductionCoordinator
| "Remote Recording Engineer" -> RemoteRecordingEngineer
| "Reporter" -> Reporter
| "Researcher" -> Researcher
| "Sales Manager" -> SalesManager
| "Sales Representative" -> SalesRepresentative
| "Script Coordinator" -> ScriptCoordinator
| "Script Editor" -> ScriptEditor
| "Senior Producer" -> SeniorProducer
| "Social Media Manager" -> SocialMediaManager
| "Songwriter" -> Songwriter
| "Sound Designer" -> SoundDesigner
| "Story Editor" -> StoryEditor
| "Studio Coordinator" -> StudioCoordinator
| "Technical Director" -> TechnicalDirector
| "Technical Manager" -> TechnicalManager
| "Theme Music" -> ThemeMusic
| "Transcriber" -> Transcriber
| "Translator" -> Translator
| "Editor (Video)" -> VideoEditor
| "Voice Actor" -> VoiceActor
| "Writer" -> Writer
| "Editor (Writing)" -> WritingEditor
| _ -> invalidArg (nameof role) $"{role} is not a valid role"
/// <summary>Role / group cross-reference</summary>
static member GroupXref =
[ Announcer, Cast
AssistantCamera, VideoProduction
AssistantDirector, CreativeDirection
AssistantEditor, VideoPostProduction
AssociateProducer, CreativeDirection
AudioEditor, AudioPostProduction
AudioEngineer, AudioProduction
Author, Writing
BookingCoordinator, Administration
CameraGrip, VideoProduction
CameraOperator, VideoProduction
CoHost, Cast
Composer, AudioPostProduction
ContentManager, Administration
Consultant, Misc
CoverArtDesigner, Visuals
CoWriter, Writing
CreativeDirector, CreativeDirection
DevelopmentProducer, CreativeDirection
Director, CreativeDirection
EditorialDirector, Writing
ExecutiveProducer, CreativeDirection
FactChecker, Writing
FoleyArtist, AudioPostProduction
GraphicDesigner, Visuals
Guest, Cast
GuestHost, Cast
GuestWriter, Writing
Host, Cast
Intern, Misc
LightingDesigner, VideoProduction
Logger, Writing
ManagingEditor, Writing
MarketingManager, Administration
MusicContributor, AudioPostProduction
MusicProduction, AudioPostProduction
Narrator, Cast
PostProductionEngineer, AudioProduction
Producer, CreativeDirection
ProductionAssistant, Administration
ProductionCoordinator, Administration
RemoteRecordingEngineer, AudioProduction
Reporter, Cast
Researcher, Writing
SalesManager, Administration
SalesRepresentative, Administration
ScriptCoordinator, Writing
ScriptEditor, Writing
SeniorProducer, CreativeDirection
SocialMediaManager, Community
Songwriter, Writing
SoundDesigner, AudioPostProduction
StoryEditor, Writing
StudioCoordinator, AudioProduction
TechnicalDirector, AudioProduction
TechnicalManager, AudioProduction
ThemeMusic, AudioPostProduction
Transcriber, Writing
Translator, Writing
VideoEditor, VideoPostProduction
VoiceActor, Cast
Writer, Writing
WritingEditor, Writing ]
/// <inheritdoc />
override this.ToString() =
match this with
| Announcer -> "Announcer"
| AssistantCamera -> "Assistant Camera"
| AssistantDirector -> "Assistant Director"
| AssistantEditor -> "Assistant Editor"
| AssociateProducer -> "Associate Producer"
| AudioEditor -> "Audio Editor"
| AudioEngineer -> "Audio Engineer"
| Author -> "Author"
| BookingCoordinator -> "Booking Coordinator"
| CameraGrip -> "Camera Grip"
| CameraOperator -> "Camera Operator"
| CoHost -> "Co-Host"
| Composer -> "Composer"
| Consultant -> "Consultant"
| ContentManager -> "Content Manager"
| CoverArtDesigner -> "Cover Art Designer"
| CoWriter -> "Co-Writer"
| CreativeDirector -> "Creative Director"
| DevelopmentProducer -> "Development Producer"
| Director -> "Director"
| EditorialDirector -> "Editorial Director"
| ExecutiveProducer -> "Executive Producer"
| FactChecker -> "Fact Checker"
| FoleyArtist -> "Foley Artist"
| GraphicDesigner -> "Graphic Designer"
| Guest -> "Guest"
| GuestHost -> "Guest Host"
| GuestWriter -> "Guest Writer"
| Host -> "Host"
| Intern -> "Intern"
| LightingDesigner -> "Lighting Designer"
| Logger -> "Logger"
| ManagingEditor -> "Managing Editor"
| MarketingManager -> "Marketing Manager"
| MusicContributor -> "Music Contributor"
| MusicProduction -> "Music Production"
| Narrator -> "Narrator"
| PostProductionEngineer -> "Post Production Engineer"
| Producer -> "Producer"
| ProductionAssistant -> "Production Assistant"
| ProductionCoordinator -> "Production Coordinator"
| RemoteRecordingEngineer -> "Remote Recording Engineer"
| Reporter -> "Reporter"
| Researcher -> "Researcher"
| SalesManager -> "Sales Manager"
| SalesRepresentative -> "Sales Representative"
| ScriptCoordinator -> "Script Coordinator"
| ScriptEditor -> "Script Editor"
| SeniorProducer -> "Senior Producer"
| SocialMediaManager -> "Social Media Manager"
| Songwriter -> "Songwriter"
| SoundDesigner -> "Sound Designer"
| StoryEditor -> "Story Editor"
| StudioCoordinator -> "Studio Coordinator"
| TechnicalDirector -> "Technical Director"
| TechnicalManager -> "Technical Manager"
| ThemeMusic -> "Theme Music"
| Transcriber -> "Transcriber"
| Translator -> "Translator"
| VideoEditor -> "Editor (Video)"
| VoiceActor -> "Voice Actor"
| Writer -> "Writer"
| WritingEditor -> "Editor (Writing)"
/// <summary>Get the display name of the role</summary>
/// <remarks>Both Video Production and Writing have an "Editor"; this is how it should be rendered in RSS feeds and
/// in the UI</remarks>
member this.ToDisplay() =
match this with VideoEditor | WritingEditor -> "Editor" | _ -> string this
/// <summary>Get the <c>PersonGroup</c> to which this role belongs</summary>
member this.Group =
PersonRole.GroupXref |> List.find (fun it -> fst it = this) |> snd
open NodaTime.Text
/// <summary>A podcast episode</summary>
type Episode = {
/// <summary>The URL to the media file for the episode (may be permalink)</summary>
Media: string
/// <summary>The length of the media file, in bytes</summary>
Length: int64
/// <summary>The duration of the episode</summary>
Duration: Duration option
/// <summary>The media type of the file (overrides podcast default if present)</summary>
MediaType: string option
/// <summary>The URL to the image file for this episode (overrides podcast image if present, may be permalink)</summary>
ImageUrl: string option
/// <summary>A subtitle for this episode</summary>
Subtitle: string option
/// <summary>This episode's explicit rating (overrides podcast rating if present)</summary>
Explicit: ExplicitRating option
/// <summary>Chapters for this episode</summary>
Chapters: Chapter list option
/// <summary>A link to a chapter file</summary>
ChapterFile: string option
/// <summary>The MIME type for the chapter file</summary>
ChapterType: string option
/// <summary>Whether the chapters have locations that should be displayed as waypoints</summary>
ChapterWaypoints: bool option
/// <summary>The URL for the transcript of the episode (may be permalink)</summary>
TranscriptUrl: string option
/// <summary>The MIME type of the transcript</summary>
TranscriptType: string option
/// <summary>The language in which the transcript is written</summary>
TranscriptLang: string option
/// <summary>If true, the transcript will be declared (in the feed) to be a captions file</summary>
TranscriptCaptions: bool option
/// <summary>The season number (for serialized podcasts)</summary>
SeasonNumber: int option
/// <summary>A description of the season</summary>
SeasonDescription: string option
/// <summary>The episode number</summary>
EpisodeNumber: double option
/// <summary>A description of the episode</summary>
EpisodeDescription: string option
} with
/// <summary>An empty episode</summary>
static member Empty =
{ Media = ""
Length = 0L
Duration = None
MediaType = None
ImageUrl = None
Subtitle = None
Explicit = None
Chapters = None
ChapterFile = None
ChapterType = None
ChapterWaypoints = None
TranscriptUrl = None
TranscriptType = None
TranscriptLang = None
TranscriptCaptions = None
SeasonNumber = None
SeasonDescription = None
EpisodeNumber = None
EpisodeDescription = None }
/// <summary>Format a duration for an episode</summary>
/// <returns>A duration formatted in hours, minutes, and seconds</returns>
member this.FormatDuration() =
this.Duration |> Option.map (DurationPattern.CreateWithInvariantCulture("H:mm:ss").Format)
/// <summary>PodcastIndex.org podcast:medium allowed values</summary>
[<Struct>]
type PodcastMedium =
| Podcast
| Music
| Video
| Film
| Audiobook
| Newsletter
| Blog
/// <summary>Parse a string into a podcast medium</summary>
/// <param name="medium">The string to be parsed</param>
/// <returns>The <c>PodcastMedium</c> parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse medium =
match medium with
| "podcast" -> Podcast
| "music" -> Music
| "video" -> Video
| "film" -> Film
| "audiobook" -> Audiobook
| "newsletter" -> Newsletter
| "blog" -> Blog
| _ -> invalidArg (nameof medium) $"{medium} is not a valid podcast medium"
/// <inheritdoc />
override this.ToString() =
match this with
| Podcast -> "podcast"
| Music -> "music"
| Video -> "video"
| Film -> "film"
| Audiobook -> "audiobook"
| Newsletter -> "newsletter"
| Blog -> "blog"
-524
View File
@@ -203,492 +203,6 @@ type CommentStatus =
match this with Approved -> "Approved" | Pending -> "Pending" | Spam -> "Spam"
/// <summary>Valid values for the iTunes explicit rating</summary>
[<Struct>]
type ExplicitRating =
| Yes
| No
| Clean
/// <summary>Parse a string into an explicit rating</summary>
/// <param name="rating">The string representation of the rating</param>
/// <returns>The <c>ExplicitRating</c> parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse rating =
match rating with
| "yes" -> Yes
| "no" -> No
| "clean" -> Clean
| _ -> invalidArg (nameof rating) $"{rating} is not a valid explicit rating"
/// <inheritdoc />
override this.ToString() =
match this with Yes -> "yes" | No -> "no" | Clean -> "clean"
/// <summary>A location (specified by Podcast Index)</summary>
type Location = {
/// <summary>The name of the location (free-form text)</summary>
Name: string
/// <summary>A geographic coordinate string (RFC 5870)</summary>
Geo: string
/// <summary>An OpenStreetMap query</summary>
Osm: string option
}
/// <summary>A chapter in a podcast episode</summary>
type Chapter = {
/// <summary>The start time for the chapter</summary>
StartTime: Duration
/// <summary>The title for this chapter</summary>
Title: string option
/// <summary>A URL for an image for this chapter</summary>
ImageUrl: string option
/// <summary>A URL with information pertaining to this chapter</summary>
Url: string option
/// <summary>Whether this chapter is hidden</summary>
IsHidden: bool option
/// <summary>The episode end time for the chapter</summary>
EndTime: Duration option
/// <summary>A location that applies to a chapter</summary>
Location: Location option
} with
/// <summary>An empty chapter</summary>
static member Empty =
{ StartTime = Duration.Zero
Title = None
ImageUrl = None
Url = None
IsHidden = None
EndTime = None
Location = None }
/// <summary>A group to which a podcast person can belong</summary>
type PersonGroup =
| Administration
| AudioPostProduction
| AudioProduction
| Cast
| Community
| CreativeDirection
| Misc
| VideoPostProduction
| VideoProduction
| Visuals
| Writing
/// <summary>Parse a string into a group</summary>
/// <param name="group">The string representation of the group</param>
/// <returns>The <c>PersonGroup</c> instance parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse group =
match group with
| "Administration" -> Administration
| "Audio Post-Production" -> AudioPostProduction
| "Audio Production" -> AudioProduction
| "Cast" -> Cast
| "Community" -> Community
| "Creative Direction" -> CreativeDirection
| "Misc." -> Misc
| "Video Post-Production" -> VideoPostProduction
| "Video Production" -> VideoProduction
| "Visuals" -> Visuals
| "Writing" -> Writing
| _ -> invalidArg (nameof group) $"{group} is not a valid group"
/// <inheritdoc />
override this.ToString() =
match this with
| Administration -> "Administration"
| AudioPostProduction -> "Audio Post-Production"
| AudioProduction -> "Audio Production"
| Cast -> "Cast"
| Community -> "Community"
| CreativeDirection -> "Creative Direction"
| Misc -> "Misc."
| VideoPostProduction -> "Video Post-Production"
| VideoProduction -> "Video Production"
| Visuals -> "Visuals"
| Writing -> "Writing"
/// <summary>The role a person plays for a podcast or a podcast episode</summary>
type PersonRole =
| Announcer
| AssistantCamera
| AssistantDirector
| AssistantEditor
| AssociateProducer
| AudioEditor
| AudioEngineer
| Author
| BookingCoordinator
| CameraGrip
| CameraOperator
| CoHost
| Composer
| Consultant
| ContentManager
| CoverArtDesigner
| CoWriter
| CreativeDirector
| DevelopmentProducer
| Director
| EditorialDirector
| ExecutiveProducer
| FactChecker
| FoleyArtist
| GraphicDesigner
| Guest
| GuestHost
| GuestWriter
| Host
| Intern
| LightingDesigner
| Logger
| ManagingEditor
| MarketingManager
| MusicContributor
| MusicProduction
| Narrator
| PostProductionEngineer
| Producer
| ProductionAssistant
| ProductionCoordinator
| RemoteRecordingEngineer
| Reporter
| Researcher
| SalesManager
| SalesRepresentative
| ScriptCoordinator
| ScriptEditor
| SeniorProducer
| SocialMediaManager
| Songwriter
| SoundDesigner
| StoryEditor
| StudioCoordinator
| TechnicalDirector
| TechnicalManager
| ThemeMusic
| Transcriber
| Translator
| VideoEditor
| VoiceActor
| Writer
| WritingEditor
/// <summary>Parse a string into a role</summary>
/// <param name="role">The string representation of the role</param>
/// <returns>The <c>PersonRole</c> instance parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse role =
match role with
| "Announcer" -> Announcer
| "Assistant Camera" -> AssistantCamera
| "Assistant Director" -> AssistantDirector
| "Assistant Editor" -> AssistantEditor
| "Associate Producer" -> AssociateProducer
| "Audio Editor" -> AudioEditor
| "Audio Engineer" -> AudioEngineer
| "Author" -> Author
| "Booking Coordinator" -> BookingCoordinator
| "Camera Grip" -> CameraGrip
| "Camera Operator" -> CameraOperator
| "Co-Host" -> CoHost
| "Composer" -> Composer
| "Consultant" -> Consultant
| "Content Manager" -> ContentManager
| "Cover Art Designer" -> CoverArtDesigner
| "Co-Writer" -> CoWriter
| "Creative Director" -> CreativeDirector
| "Development Producer" -> DevelopmentProducer
| "Director" -> Director
| "Editorial Director" -> EditorialDirector
| "Executive Producer" -> ExecutiveProducer
| "Fact Checker" -> FactChecker
| "Foley Artist" -> FoleyArtist
| "Graphic Designer" -> GraphicDesigner
| "Guest" -> Guest
| "Guest Host" -> GuestHost
| "Guest Writer" -> GuestWriter
| "Host" -> Host
| "Intern" -> Intern
| "Lighting Designer" -> LightingDesigner
| "Logger" -> Logger
| "Managing Editor" -> ManagingEditor
| "Marketing Manager" -> MarketingManager
| "Music Contributor" -> MusicContributor
| "Music Production" -> MusicProduction
| "Narrator" -> Narrator
| "Post Production Engineer" -> PostProductionEngineer
| "Producer" -> Producer
| "Production Assistant" -> ProductionAssistant
| "Production Coordinator" -> ProductionCoordinator
| "Remote Recording Engineer" -> RemoteRecordingEngineer
| "Reporter" -> Reporter
| "Researcher" -> Researcher
| "Sales Manager" -> SalesManager
| "Sales Representative" -> SalesRepresentative
| "Script Coordinator" -> ScriptCoordinator
| "Script Editor" -> ScriptEditor
| "Senior Producer" -> SeniorProducer
| "Social Media Manager" -> SocialMediaManager
| "Songwriter" -> Songwriter
| "Sound Designer" -> SoundDesigner
| "Story Editor" -> StoryEditor
| "Studio Coordinator" -> StudioCoordinator
| "Technical Director" -> TechnicalDirector
| "Technical Manager" -> TechnicalManager
| "Theme Music" -> ThemeMusic
| "Transcriber" -> Transcriber
| "Translator" -> Translator
| "Editor (Video)" -> VideoEditor
| "Voice Actor" -> VoiceActor
| "Writer" -> Writer
| "Editor (Writing)" -> WritingEditor
| _ -> invalidArg (nameof role) $"{role} is not a valid role"
/// <inheritdoc />
override this.ToString() =
match this with
| Announcer -> "Announcer"
| AssistantCamera -> "Assistant Camera"
| AssistantDirector -> "Assistant Director"
| AssistantEditor -> "Assistant Editor"
| AssociateProducer -> "Associate Producer"
| AudioEditor -> "Audio Editor"
| AudioEngineer -> "Audio Engineer"
| Author -> "Author"
| BookingCoordinator -> "Booking Coordinator"
| CameraGrip -> "Camera Grip"
| CameraOperator -> "Camera Operator"
| CoHost -> "Co-Host"
| Composer -> "Composer"
| Consultant -> "Consultant"
| ContentManager -> "Content Manager"
| CoverArtDesigner -> "Cover Art Designer"
| CoWriter -> "Co-Writer"
| CreativeDirector -> "Creative Director"
| DevelopmentProducer -> "Development Producer"
| Director -> "Director"
| EditorialDirector -> "Editorial Director"
| ExecutiveProducer -> "Executive Producer"
| FactChecker -> "Fact Checker"
| FoleyArtist -> "Foley Artist"
| GraphicDesigner -> "Graphic Designer"
| Guest -> "Guest"
| GuestHost -> "Guest Host"
| GuestWriter -> "Guest Writer"
| Host -> "Host"
| Intern -> "Intern"
| LightingDesigner -> "Lighting Designer"
| Logger -> "Logger"
| ManagingEditor -> "Managing Editor"
| MarketingManager -> "Marketing Manager"
| MusicContributor -> "Music Contributor"
| MusicProduction -> "Music Production"
| Narrator -> "Narrator"
| PostProductionEngineer -> "Post Production Engineer"
| Producer -> "Producer"
| ProductionAssistant -> "Production Assistant"
| ProductionCoordinator -> "Production Coordinator"
| RemoteRecordingEngineer -> "Remote Recording Engineer"
| Reporter -> "Reporter"
| Researcher -> "Researcher"
| SalesManager -> "Sales Manager"
| SalesRepresentative -> "Sales Representative"
| ScriptCoordinator -> "Script Coordinator"
| ScriptEditor -> "Script Editor"
| SeniorProducer -> "Senior Producer"
| SocialMediaManager -> "Social Media Manager"
| Songwriter -> "Songwriter"
| SoundDesigner -> "Sound Designer"
| StoryEditor -> "Story Editor"
| StudioCoordinator -> "Studio Coordinator"
| TechnicalDirector -> "Technical Director"
| TechnicalManager -> "Technical Manager"
| ThemeMusic -> "Theme Music"
| Transcriber -> "Transcriber"
| Translator -> "Translator"
| VideoEditor -> "Editor (Video)"
| VoiceActor -> "Voice Actor"
| Writer -> "Writer"
| WritingEditor -> "Editor (Writing)"
/// <summary>Get the display name of the role</summary>
/// <remarks>Both Video Production and Writing have an "Editor"; this is how it should be rendered in RSS feeds and
/// in the UI</remarks>
member this.ToDisplay() =
match this with VideoEditor | WritingEditor -> "Editor" | _ -> string this
/// <summary>Role / group cross-reference</summary>
static member GroupXref =
[ Announcer, Cast
AssistantCamera, VideoProduction
AssistantDirector, CreativeDirection
AssistantEditor, VideoPostProduction
AssociateProducer, CreativeDirection
AudioEditor, AudioPostProduction
AudioEngineer, AudioProduction
Author, Writing
BookingCoordinator, Administration
CameraGrip, VideoProduction
CameraOperator, VideoProduction
CoHost, Cast
Composer, AudioPostProduction
ContentManager, Administration
Consultant, Misc
CoverArtDesigner, Visuals
CoWriter, Writing
CreativeDirector, CreativeDirection
DevelopmentProducer, CreativeDirection
Director, CreativeDirection
EditorialDirector, Writing
ExecutiveProducer, CreativeDirection
FactChecker, Writing
FoleyArtist, AudioPostProduction
GraphicDesigner, Visuals
Guest, Cast
GuestHost, Cast
GuestWriter, Writing
Host, Cast
Intern, Misc
LightingDesigner, VideoProduction
Logger, Writing
ManagingEditor, Writing
MarketingManager, Administration
MusicContributor, AudioPostProduction
MusicProduction, AudioPostProduction
Narrator, Cast
PostProductionEngineer, AudioProduction
Producer, CreativeDirection
ProductionAssistant, Administration
ProductionCoordinator, Administration
RemoteRecordingEngineer, AudioProduction
Reporter, Cast
Researcher, Writing
SalesManager, Administration
SalesRepresentative, Administration
ScriptCoordinator, Writing
ScriptEditor, Writing
SeniorProducer, CreativeDirection
SocialMediaManager, Community
Songwriter, Writing
SoundDesigner, AudioPostProduction
StoryEditor, Writing
StudioCoordinator, AudioProduction
TechnicalDirector, AudioProduction
TechnicalManager, AudioProduction
ThemeMusic, AudioPostProduction
Transcriber, Writing
Translator, Writing
VideoEditor, VideoPostProduction
VoiceActor, Cast
Writer, Writing
WritingEditor, Writing ]
open NodaTime.Text
/// <summary>A podcast episode</summary>
type Episode = {
/// <summary>The URL to the media file for the episode (may be permalink)</summary>
Media: string
/// <summary>The length of the media file, in bytes</summary>
Length: int64
/// <summary>The duration of the episode</summary>
Duration: Duration option
/// <summary>The media type of the file (overrides podcast default if present)</summary>
MediaType: string option
/// <summary>The URL to the image file for this episode (overrides podcast image if present, may be permalink)</summary>
ImageUrl: string option
/// <summary>A subtitle for this episode</summary>
Subtitle: string option
/// <summary>This episode's explicit rating (overrides podcast rating if present)</summary>
Explicit: ExplicitRating option
/// <summary>Chapters for this episode</summary>
Chapters: Chapter list option
/// <summary>A link to a chapter file</summary>
ChapterFile: string option
/// <summary>The MIME type for the chapter file</summary>
ChapterType: string option
/// <summary>Whether the chapters have locations that should be displayed as waypoints</summary>
ChapterWaypoints: bool option
/// <summary>The URL for the transcript of the episode (may be permalink)</summary>
TranscriptUrl: string option
/// <summary>The MIME type of the transcript</summary>
TranscriptType: string option
/// <summary>The language in which the transcript is written</summary>
TranscriptLang: string option
/// <summary>If true, the transcript will be declared (in the feed) to be a captions file</summary>
TranscriptCaptions: bool option
/// <summary>The season number (for serialized podcasts)</summary>
SeasonNumber: int option
/// <summary>A description of the season</summary>
SeasonDescription: string option
/// <summary>The episode number</summary>
EpisodeNumber: double option
/// <summary>A description of the episode</summary>
EpisodeDescription: string option
} with
/// <summary>An empty episode</summary>
static member Empty =
{ Media = ""
Length = 0L
Duration = None
MediaType = None
ImageUrl = None
Subtitle = None
Explicit = None
Chapters = None
ChapterFile = None
ChapterType = None
ChapterWaypoints = None
TranscriptUrl = None
TranscriptType = None
TranscriptLang = None
TranscriptCaptions = None
SeasonNumber = None
SeasonDescription = None
EpisodeNumber = None
EpisodeDescription = None }
/// <summary>Format a duration for an episode</summary>
/// <returns>A duration formatted in hours, minutes, and seconds</returns>
member this.FormatDuration() =
this.Duration |> Option.map (DurationPattern.CreateWithInvariantCulture("H:mm:ss").Format)
/// <summary>Types of markup text</summary>
type MarkupText =
/// <summary>Markdown text</summary>
@@ -1069,44 +583,6 @@ type PageId =
match this with PageId it -> it
/// <summary>PodcastIndex.org podcast:medium allowed values</summary>
[<Struct>]
type PodcastMedium =
| Podcast
| Music
| Video
| Film
| Audiobook
| Newsletter
| Blog
/// <summary>Parse a string into a podcast medium</summary>
/// <param name="medium">The string to be parsed</param>
/// <returns>The <c>PodcastMedium</c> parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse medium =
match medium with
| "podcast" -> Podcast
| "music" -> Music
| "video" -> Video
| "film" -> Film
| "audiobook" -> Audiobook
| "newsletter" -> Newsletter
| "blog" -> Blog
| _ -> invalidArg (nameof medium) $"{medium} is not a valid podcast medium"
/// <inheritdoc />
override this.ToString() =
match this with
| Podcast -> "podcast"
| Music -> "music"
| Video -> "video"
| Film -> "film"
| Audiobook -> "audiobook"
| Newsletter -> "newsletter"
| Blog -> "blog"
/// <summary>Statuses for posts</summary>
[<Struct>]
type PostStatus =