podcast
iTunes Compliant and RSS 2.0 Podcast Generator in Golang
About podcast
This large README is shown as a plain-text preview. Read the full README at the source
[](https://godoc.org/github.com/eduncan911/podcast)
[](https://github.com/eduncan911/podcast/actions?workflow=go-cicd)
[](https://coveralls.io/github/eduncan911/podcast?branch=master)
[](https://goreportcard.com/report/github.com/eduncan911/podcast)
[](https://eduncan911.mit-license.org/)
# podcast
Package podcast generates a fully compliant iTunes and RSS 2.0 podcast feed
for GoLang using a simple API.
Full documentation with detailed examples located at <a href="https://godoc.org/github.com/eduncan911/podcast">https://godoc.org/github.com/eduncan911/podcast</a>
### Usage
To use, `go get` and `import` the package like your typical GoLang library.
$ go get -u github.com/eduncan911/podcast
import "github.com/eduncan911/podcast"
The API exposes a number of method receivers on structs that implements the
logic required to comply with the specifications and ensure a compliant feed.
A number of overrides occur to help with iTunes visibility of your episodes.
Notably, the `Podcast.AddItem` function performs most
of the heavy lifting by taking the `Item` input and performing
validation, overrides and duplicate setters through the feed.
Full detailed Examples of the API are at <a href="https://godoc.org/github.com/eduncan911/podcast">https://godoc.org/github.com/eduncan911/podcast</a>.
### Go Modules
This library is supported on GoLang 1.7 and higher.
We have implemented Go Modules support and the CI pipeline shows it working with
new installs, tested with Go 1.13. To keep 1.7 compatibility, we use
`go mod vendor` to maintain the `vendor/` folder for older 1.7 and later runtimes.
If either runtime has an issue, please create an Issue and I will address.
### Extensibility
For version 1.x, you are not restricted in having full control over your feeds.
You may choose to skip the API methods and instead use the structs directly. The
fields have been grouped by RSS 2.0 and iTunes fields with iTunes specific fields
all prefixed with the letter `I`.
However, do note that the 2.x version currently in progress will break this
extensibility and enforce API methods going forward. This is to ensure that the feed
can both be marshalled, and unmarshalled back and forth (current 1.x branch can only
be unmarshalled - hence the work for 2.x).
### Fuzzing Inputs
`go-fuzz` has been added in 1.4.1, covering all exported API methods. They have been
ran extensively and no issues have come out of them yet (most tests were ran overnight,
over about 11 hours with zero crashes).
If you wish to help fuzz the inputs, with Go 1.13 or later you can run `go-fuzz` on any
of the inputs.
go get -u github.com/dvyukov/go-fuzz/go-fuzz
go get -u github.com/dvyukov/go-fuzz/go-fuzz-build
go get -u github.com/eduncan911/podcast
cd $GOPATH/src/github.com/eduncan911/podcast
go-fuzz-build
go-fuzz -func FuzzPodcastAddItem
To obtain a list of available funcs to pass, just run `go-fuzz` without any parameters:
$ go-fuzz
2020/02/13 07:27:32 -func flag not provided, but multiple fuzz functions available:
FuzzItemAddDuration, FuzzItemAddEnclosure, FuzzItemAddImage, FuzzItemAddPubDate,
FuzzItemAddSummary, FuzzPodcastAddAtomLink, FuzzPodcastAddAuthor, FuzzPodcastAddCategory,
FuzzPodcastAddImage, FuzzPodcastAddItem, FuzzPodcastAddLastBuildDate, FuzzPodcastAddPubDate,
FuzzPodcastAddSubTitle, FuzzPodcastAddSummary, FuzzPodcastBytes, FuzzPodcastEncode,
FuzzPodcastNew
If you do find an issue, please raise an issue immediately and I will quickly address.
### Roadmap
The 1.x branch is now mostly in maintenance mode, open to PRs. This means no
more planned features on the 1.x feature branch is expected. With the success of 6
iTunes-accepted podcasts I have published with this library, and with the feedback from
the community, the 1.x releases are now considered stable.
The 2.x branch's primary focus is to allow for bi-direction marshalling both ways.
Currently, the 1.x branch only allows unmarshalling to a serial feed. An attempt to marshall
a serialized feed back into a Podcast form will error or not work correctly. Note that while
the 2.x branch is targeted to remain backwards compatible, it is true if using the public
API funcs to set parameters only. Several of the underlying public fields are being removed
in order to accommodate the marshalling of serialized data. Therefore, a version 2.x is denoted
for this release.
### Versioning
We use SemVer versioning schema. You can rest assured that pulling 1.x branches will
remain backwards compatible now and into the future.
However, the new 2.x branch, while keeping the same API, is expected break those that
bypass the API methods and use the underlying public properties instead.
### Release Notes
v1.4.2
* Slim down Go Modules for consumers (#32)
v1.4.1
* Implement fuzz logic testing of exported funcs (#31)
* Upgrade CICD Pipeline Tooling (#31)
* Update documentation for 1.x and 2.3 (#31)
* Allow godoc2ghmd to run without network (#31)
v1.4.0
* Add Go Modules, Update vendor folder (#26, #25)
* Add C.I. GitHub Actions (#25)
* Add additional error checks found by linters (#25)
* Go Fmt enclosure_test.go (#25)
v1.3.2
* Correct count len of UTF8 strings (#9)
* Implement duration parser (#8)
* Fix Github and GoDocs Markdown (#14)
* Move podcast.go Private Methods to Respected Files (#12)
* Allow providing GUID on Podcast (#15)
v1.3.1
* increased itunes compliance after feedback from Apple:
- specified what categories should be set with AddCategory().
- enforced title and link as part of Image.
* added Podcast.AddAtomLink() for more broad compliance to readers.
v1.3.0
* fixes Item.Duration being set incorrectly.
* changed Item.AddEnclosure() parameter definition (Bytes not Seconds!).
* added Item.AddDuration formatting and override.
* added more documentation surrounding Item.Enclosure{}
v1.2.1
* added Podcast.AddSubTitle() and truncating to 64 chars.
* added a number of Guards to protect against empty fields.
v1.2.0
* added Podcast.AddPubDate() and Podcast.AddLastBuildDate() overrides.
* added Item.AddImage() to mask some cumbersome addition of IImage.
* added Item.AddPubDate to simply datetime setters.
* added more examples (mostly around Item struct).
* tweaked some documentation.
v1.1.0
* Enabling CDATA in ISummary fields for Podcast and Channel.
v1.0.0
* Initial release.
* Full documentation, full examples and complete code coverage.
### References
RSS 2.0: <a href="https://cyber.harvard.edu/rss/rss.html">https://cyber.harvard.edu/rss/rss.html</a>
Podcasts: <a href="https://help.apple.com/itc/podcasts_connect/#/itca5b22233">https://help.apple.com/itc/podcasts_connect/#/itca5b22233</a>
#### Example:
<details>
<summary>Click to expand code.</summary>
```go
// ResponseWriter example using Podcast.Encode(w io.Writer).
//
httpHandler := func(w http.ResponseWriter, r *http.Request) {
// instantiate a new Podcast
p := podcast.New(
"eduncan911 Podcasts",
"http://eduncan911.com/",
"An example Podcast",
&pubDate, &updatedDate,
)
// add some channel properties
p.AddAuthor("Jane Doe", "[email protected]")
p.AddAtomLink("http://eduncan911.com/feed.rss")
p.AddImage("http://janedoe.com/i.jpg")
p.AddSummary(`link <a href="http://example.com">example.com</a>`)
p.IExplicit = "no"
for i := int64(1); i < 3; i++ {
n := strconv.FormatInt(i, 10)
d := pubDate.AddDate(0, 0, int(i))
// create an Item
item := podcast.Item{
Title: "Episode " + n,
Link: "http://example.com/" + n + ".mp3",
Description: "Description for Episode " + n,
PubDate: &d,
}
item.AddImage("http://example.com/episode-" + n + ".png")
item.AddSummary(`item <a href="http://example.com">example.com</a>`)
// add a Download to the Item
item.AddEnclosure("http://e.com/"+n+".mp3", podcast.MP3, 55*(i+1))
// add the Item and check for validation errors
if _, err := p.AddItem(item); err != nil {
fmt.Println(item.Title, ": error", err.Error())
return
}
}
// set the Content Type to that of XML
w.Header().Set("Content-Type", "application/xml")
// finally, Encode and write the Podcast to the ResponseWriter.
//
// a simple pattern is to handle any errors within this check.
// alternatively if using middleware, you can just return
// the Podcast entity as it also implements the io.Writer interface
// that complies with several middleware packages.
if err := p.Encode(w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
rr := httptest.NewRecorder()
httpHandler(rr, nil)
os.Stdout.Write(rr.Body.Bytes())
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
// <channel>
// <title>eduncan911 Podcasts</title>
// <link>http://eduncan911.com/</link>
// <description>An example Podcast</description>
// <generator>go podcast v1.3.1 (github.com/eduncan911/podcast)</generator>
// <language>en-us</language>
// <lastBuildDate>Mon, 06 Feb 2017 08:21:52 +0000</lastBuildDate>
// <managingEditor>[email protected] (Jane Doe)</managingEditor>
// <pubDate>Sat, 04 Feb 2017 08:21:52 +0000</pubDate>
// <image>
// <url>http://janedoe.com/i.jpg</url>
// <title>eduncan911 Podcasts</title>
// <link>http://eduncan911.com/</link>
// </image>
// <atom:link href="http://eduncan911.com/feed.rss" rel="self" type="application/rss+xml"></atom:link>
// <itunes:author>[email protected] (Jane Doe)</itunes:author>
// <itunes:summary><![CDATA[link <a href="http://example.com">example.com</a>]]></itunes:summary>
// <itunes:image href="http://janedoe.com/i.jpg"></itunes:image>
// <itunes:explicit>no</itunes:explicit>
// <item>
// <guid>http://e.com/1.mp3</guid>
// <title>Episode 1</title>
// <link>http://example.com/1.mp3</link>
// <description>Description for Episode 1</description>
// <pubDate>Sun, 05 Feb 2017 08:21:52 +0000</pubDate>
// <enclosure url="http://e.com/1.mp3" length="110" type="audio/mpeg"></enclosure>
// <itunes:author>[email protected] (Jane Doe)</itunes:author>
// <itunes:summary><![CDATA[item <a href="http://example.com">example.com</a>]]></itunes:summary>
// <itunes:image href="http://example.com/episode-1.png"></itunes:image>
// </item>
// <item>
// <guid>http://e.com/2.mp3</guid>
// <title>Episode 2</title>
// <link>http://example.com/2.mp3</link>
// <description>Description for Episode 2</description>
// <pubDate>Mon, 06 Feb 2017 08:21:52 +0000</pubDate>
// <enclosure url="http://e.com/2.mp3" length="165" type="audio/mpeg"></enclosure>
// <itunes:author>[email protected] (Jane Doe)</itunes:author>
// <itunes:summary><![CDATA[item <a href="http://example.com">example.com</a>]]></itunes:summary>
// <itunes:image href="http://example.com/episode-2.png"></itunes:image>
// </item>
// </channel>
// </rss>
```
</details>
#### Example:
<details>
<summary>Click to expand code.</summary>
```go
// instantiate a new Podcast
p := podcast.New(
"Sample Podcasts",
"http://example.com/",
"An example Podcast",
&createdDate, &updatedDate,
)
// add some channel properties
p.ISubtitle = "A simple Podcast"
p.AddSummary(`link <a href="http://example.com">example.com</a>`)
p.AddImage("http://example.com/podcast.jpg")
p.AddAuthor("Jane Doe", "[email protected]")
p.AddAtomLink("http://example.com/atom.rss")
for i := int64(9); i < 11; i++ {
n := strconv.FormatInt(i, 10)
d := pubDate.AddDate(0, 0, int(i))
// create an Item
item := podcast.Item{
Title: "Episode " + n,
Description: "Description for Episode " + n,
ISubtitle: "A simple episode " + n,
PubDate: &d,
}
item.AddImage("http://example.com/episode-" + n + ".png")
item.AddSummary(`item k <a href="http://example.com">example.com</a>`)
// add a Download to the Item
item.AddEnclosure("http://example.com/"+n+".mp3", podcast.MP3, 55*(i+1))
// add the Item and check for validation errors
if _, err := p.AddItem(item); err != nil {
os.Stderr.WriteString("item validation error: " + err.Error())
}
}
// Podcast.Encode writes to an io.Writer
if err := p.Encode(os.Stdout); err != nil {
fmt.Println("error writing to stdout:", err.Error())
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
// <channel>
// <title>Sample Podcasts</title>
// <link>http://example.com/</link>
// <description>An example Podcast</description>
// <generator>go podcast v1.3.1 (github.com/eduncan911/podcast)</generator>
// <language>en-us</language>
// <lastBuildDate>Mon, 06 Feb 2017 08:21:52 +0000</lastBuildDate>
// <managingEditor>[email protected] (Jane Doe)</managingEditor>
// <pubDate>Wed, 01 Feb 2017 08:21:52 +0000</pubDate>
// <image>
// <url>http://example.com/podcast.jpg</url>
// <title>Sample Podcasts</title>
// <link>http://example.com/</link>
// </image>
// <atom:link href="http://example.com/atom.rss" rel="self" type="application/rss+xml"></atom:link>
// <itunes:author>[email protected] (Jane Doe)</itunes:author>
// <itunes:subtitle>A simple Podcast</itunes:subtitle>
// <itunes:summary><![CDATA[link <a href="http://example.com">example.com</a>]]></itunes:summary>
// <itunes:image href="http://example.com/podcast.jpg"></itunes:image>
// <item>
// <guid>http://example.com/9.mp3</guid>
// <title>Episode 9</title>
// <link>http://example.com/9.mp3</link>
// <description>Description for Episode 9</description>
// <pubDate>Mon, 13 Feb 2017 08:21:52 +0000</pubDate>
// <enclosure url="http://example.com/9.mp3" length="550" type="audio/mpeg"></enclosure>
// <itunes:author>[email protected] (Jane Doe)</itunes:author>
// <itunes:subtitle>A simple episode 9</itunes:subtitle>
// <itunes:summary><![CDATA[item k <a href="http://example.com">example.com</a>]]></itunes:summary>
// <itunes:image href="http://example.com/episode-9.png"></itunes:image>
// </item>
// <item>
// <guid>http://example.com/10.mp3</guid>
// <title>Episode 10</title>
// <link>http://example.com/10.mp3</link>
// <description>Description for Episode 10</description>
// <pubDate>Tue, 14 Feb 2017 08:21:52 +0000</pubDate>
// <enclosure url="http://example.com/10.mp3" length="605" type="audio/mpeg"></enclosure>
// <itunes:author>[email protected] (Jane Doe)</itunes:author>
// <itunes:subtitle>A simple episode 10</itunes:subtitle>
// <itunes:summary><![CDATA[item k <a href="http://example.com">example.com</a>]]></itunes:summary>
// <itunes:image href="http://example.com/episode-10.png"></itunes:image>
// </item>
// </channel>
// </rss>
```
</details>
## Table of Contents
* [Imported Packages](#pkg-imports)
* [Index](#pkg-index)
* [Examples](#pkg-examples)
## <a name="pkg-imports">Imported Packages</a>
- [github.com/pkg/errors](https://godoc.org/github.com/pkg/errors)
## <a name="pkg-index">Index</a>
* [type AtomLink](#AtomLink)
* [type Author](#Author)
* [type Enclosure](#Enclosure)
* [type EnclosureType](#EnclosureType)
* [func (et EnclosureType) String() string](#EnclosureType.String)
* [type ICategory](#ICategory)
* [type IImage](#IImage)
* [type ISummary](#ISummary)
* [type Image](#Image)
* [type Item](#Item)
* [func (i \*Item) AddDuration(durationInSeconds int64)](#Item.AddDuration)
* [func (i \*Item) AddEnclosure(url string, enclosureType EnclosureType, lengthInBytes int64)](#Item.AddEnclosure)
* [func (i \*Item) AddImage(url string)](#Item.AddImage)
* [func (i \*Item) AddPubDate(datetime \*time.Time)](#Item.AddPubDate)
* [func (i \*Item) AddSummary(summary string)](#Item.AddSummary)
* [type Podcast](#Podcast)
* [func New(title, link, description string, pubDate, lastBuildDate \*time.Time) Podcast](#New)
* [func (p \*Podcast) AddAtomLink(href string)](#Podcast.AddAtomLink)
* [func (p \*Podcast) AddAuthor(name, email string)](#Podcast.AddAuthor)
* [func (p \*Podcast) AddCategory(category string, subCategories []string)](#Podcast.AddCategory)
* [func (p \*Podcast) AddImage(url string)](#Podcast.AddImage)
* [func (p \*Podcast) AddItem(i Item) (int, error)](#Podcast.AddItem)
* [func (p \*Podcast) AddLastBuildDate(datetime \*time.Time)](#Podcast.AddLastBuildDate)
* [func (p \*Podcast) AddPubDate(datetime \*time.Time)](#Podcast.AddPubDate)
* [func (p \*Podcast) AddSubTitle(subTitle string)](#Podcast.AddSubTitle)
* [func (p \*Podcast) AddSummary(summary string)](#Podcast.AddSummary)
* [func (p \*Podcast) Bytes() []byte](#Podcast.Bytes)
* [func (p \*Podcast) Encode(w io.Writer) error](#Podcast.Encode)
* [func (p \*Podcast) String() string](#Podcast.String)
* [type TextInput](#TextInput)
#### <a name="pkg-examples">Examples</a>
* [Item.AddDuration](#example_Item_AddDuration)
* [Item.AddPubDate](#example_Item_AddPubDate)
* [New](#example_New)
* [Podcast.AddAuthor](#example_Podcast_AddAuthor)
* [Podcast.AddCategory](#example_Podcast_AddCategory)
* [Podcast.AddImage](#example_Podcast_AddImage)
* [Podcast.AddItem](#example_Podcast_AddItem)
* [Podcast.AddLastBuildDate](#example_Podcast_AddLastBuildDate)
* [Podcast.AddPubDate](#example_Podcast_AddPubDate)
* [Podcast.AddSummary](#example_Podcast_AddSummary)
* [Podcast.Bytes](#example_Podcast_Bytes)
* [Package (HttpHandlers)](#example__httpHandlers)
* [Package (IoWriter)](#example__ioWriter)
#### <a name="pkg-files">Package files</a>
[atomlink.go](./atomlink.go) [author.go](./author.go) [doc.go](./doc.go) [enclosure.go](./enclosure.go) [image.go](./image.go) [item.go](./item.go) [itunes.go](./itunes.go) [podcast.go](./podcast.go) [textinput.go](./textinput.go)
## <a name="AtomLink">type</a> [AtomLink](./atomlink.go#L6-L11)
``` go
type AtomLink struct {
XMLName xml.Name `xml:"atom:link"`
HREF string `xml:"href,attr"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
}
```
AtomLink represents the Atom reference link.
## <a name="Author">type</a> [Author](./author.go#L8-L12)
``` go
type Author struct {
XMLName xml.Name `xml:"itunes:owner"`
Name string `xml:"itunes:name"`
Email string `xml:"itunes:email"`
}
```
Author represents a named author and email.
For iTunes compliance, both Name and Email are required.
## <a name="Enclosure">type</a> [Enclosure](./enclosure.go#L46-L65)
``` go
type Enclosure struct {
XMLName xml.Name `xml:"enclosure"`
// URL is the downloadable url for the content. (Required)
URL string `xml:"url,attr"`
// Length is the size in Bytes of the download. (Required)
Length int64 `xml:"-"`
// LengthFormatted is the size in Bytes of the download. (Required)
//
// This field gets overwritten with the API when setting Length.
LengthFormatted string `xml:"length,attr"`
Frequently Asked Questions
What is podcast?
podcast is a Text Processing library for the Go programming language. iTunes Compliant and RSS 2.0 Podcast Generator in Golang
How do I install podcast?
Install podcast with the Go module system using `go get eduncan911/podcast`. Check the repository for the current installation instructions.
What category does podcast belong to?
podcast is listed under Text Processing, specifically RSS.