initial version
This commit is contained in:
parent
8779c99c96
commit
5b523d8227
|
@ -0,0 +1,7 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- stable
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -cover -v ./...
|
|
@ -0,0 +1,505 @@
|
||||||
|
|
||||||
|
|
||||||
|
# podcast
|
||||||
|
`import "github.com/eduncan911/podcast"`
|
||||||
|
|
||||||
|
* [Overview](#pkg-overview)
|
||||||
|
* [Index](#pkg-index)
|
||||||
|
* [Examples](#pkg-examples)
|
||||||
|
|
||||||
|
## <a name="pkg-overview">Overview</a>
|
||||||
|
Package `podcast` is an iTunes and RSS 2.0 podcast generator for GoLang that
|
||||||
|
enforces strict compliance by using its simple interface.
|
||||||
|
|
||||||
|
[![GoDoc](<a href="https://godoc.org/github.com/eduncan911/podcast?status.svg">https://godoc.org/github.com/eduncan911/podcast?status.svg</a>)](<a href="https://godoc.org/github.com/eduncan911/podcast">https://godoc.org/github.com/eduncan911/podcast</a>) [![Build Status](<a href="https://travis-ci.org/eduncan911/podcast.svg?branch=master">https://travis-ci.org/eduncan911/podcast.svg?branch=master</a>)](<a href="https://travis-ci.org/eduncan911/podcast">https://travis-ci.org/eduncan911/podcast</a>) [![Go Report Card](<a href="https://goreportcard.com/badge/github.com/eduncan911/podcast">https://goreportcard.com/badge/github.com/eduncan911/podcast</a>)](<a href="https://goreportcard.com/report/github.com/eduncan911/podcast">https://goreportcard.com/report/github.com/eduncan911/podcast</a>)
|
||||||
|
|
||||||
|
Full documentation with detailed examples located at [![GoDoc](<a href="https://godoc.org/github.com/eduncan911/podcast?status.svg">https://godoc.org/github.com/eduncan911/podcast?status.svg</a>)](<a href="https://godoc.org/github.com/eduncan911/podcast">https://godoc.org/github.com/eduncan911/podcast</a>)
|
||||||
|
|
||||||
|
Usage
|
||||||
|
|
||||||
|
|
||||||
|
$ go get -u 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.
|
||||||
|
|
||||||
|
See the detailed Examples in the GoDocs for complete usage.
|
||||||
|
|
||||||
|
### Extensiblity
|
||||||
|
In no way are you 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.
|
||||||
|
|
||||||
|
iTunes specific fields are all prefixed with the letter `I`.
|
||||||
|
|
||||||
|
### 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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="pkg-index">Index</a>
|
||||||
|
* [type Author](#Author)
|
||||||
|
* [type Enclosure](#Enclosure)
|
||||||
|
* [type EnclosureType](#EnclosureType)
|
||||||
|
* [func (et EnclosureType) String() string](#EnclosureType.String)
|
||||||
|
* [type ICategory](#ICategory)
|
||||||
|
* [type IImage](#IImage)
|
||||||
|
* [type Image](#Image)
|
||||||
|
* [type Item](#Item)
|
||||||
|
* [func (i *Item) AddEnclosure(url string, enclosureType EnclosureType, lengthInSeconds int64)](#Item.AddEnclosure)
|
||||||
|
* [type Podcast](#Podcast)
|
||||||
|
* [func New(title, link, description string, pubDate, lastBuildDate *time.Time) Podcast](#New)
|
||||||
|
* [func (p *Podcast) AddAuthor(a Author)](#Podcast.AddAuthor)
|
||||||
|
* [func (p *Podcast) AddCategory(category string, subCategories []string)](#Podcast.AddCategory)
|
||||||
|
* [func (p *Podcast) AddImage(i Image)](#Podcast.AddImage)
|
||||||
|
* [func (p *Podcast) AddItem(i Item) (int, error)](#Podcast.AddItem)
|
||||||
|
* [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>
|
||||||
|
* [Package](#example_)
|
||||||
|
* [New](#example_New)
|
||||||
|
* [Podcast.AddAuthor](#example_Podcast_AddAuthor)
|
||||||
|
* [Podcast.AddCategory](#example_Podcast_AddCategory)
|
||||||
|
* [Podcast.AddImage](#example_Podcast_AddImage)
|
||||||
|
* [Podcast.AddItem](#example_Podcast_AddItem)
|
||||||
|
* [Podcast.Bytes](#example_Podcast_Bytes)
|
||||||
|
* [Package (Encode)](#example__encode)
|
||||||
|
|
||||||
|
#### <a name="pkg-files">Package files</a>
|
||||||
|
[author.go](/src/github.com/eduncan911/podcast/author.go) [doc.go](/src/github.com/eduncan911/podcast/doc.go) [enclosure.go](/src/github.com/eduncan911/podcast/enclosure.go) [image.go](/src/github.com/eduncan911/podcast/image.go) [item.go](/src/github.com/eduncan911/podcast/item.go) [itunes.go](/src/github.com/eduncan911/podcast/itunes.go) [podcast.go](/src/github.com/eduncan911/podcast/podcast.go) [textinput.go](/src/github.com/eduncan911/podcast/textinput.go)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="Author">type</a> [Author](/src/target/author.go?s=149:287#L1)
|
||||||
|
``` 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 compiance, both Name and Email are required.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="Enclosure">type</a> [Enclosure](/src/target/enclosure.go?s=814:1118#L36)
|
||||||
|
``` go
|
||||||
|
type Enclosure struct {
|
||||||
|
XMLName xml.Name `xml:"enclosure"`
|
||||||
|
URL string `xml:"url,attr"`
|
||||||
|
Length int64 `xml:"-"`
|
||||||
|
LengthFormatted string `xml:"length,attr"`
|
||||||
|
Type EnclosureType `xml:"-"`
|
||||||
|
TypeFormatted string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Enclosure represents a download enclosure.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="EnclosureType">type</a> [EnclosureType](/src/target/enclosure.go?s=274:296#L11)
|
||||||
|
``` go
|
||||||
|
type EnclosureType int
|
||||||
|
```
|
||||||
|
EnclosureType specifies the type of the enclosure.
|
||||||
|
|
||||||
|
|
||||||
|
``` go
|
||||||
|
const (
|
||||||
|
M4A EnclosureType = iota
|
||||||
|
M4V
|
||||||
|
MP4
|
||||||
|
MP3
|
||||||
|
MOV
|
||||||
|
PDF
|
||||||
|
EPUB
|
||||||
|
)
|
||||||
|
```
|
||||||
|
EnclosureType specifies the type of the enclosure.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="EnclosureType.String">func</a> (EnclosureType) [String](/src/target/enclosure.go?s=371:410#L14)
|
||||||
|
``` go
|
||||||
|
func (et EnclosureType) String() string
|
||||||
|
```
|
||||||
|
String returns the MIME type encoding of the specified EnclosureType.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="ICategory">type</a> [ICategory](/src/target/itunes.go?s=645:782#L12)
|
||||||
|
``` go
|
||||||
|
type ICategory struct {
|
||||||
|
XMLName xml.Name `xml:"itunes:category"`
|
||||||
|
Text string `xml:"text,attr"`
|
||||||
|
ICategories []*ICategory
|
||||||
|
}
|
||||||
|
```
|
||||||
|
ICategory is a 2-tier classification system for iTunes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="IImage">type</a> [IImage](/src/target/itunes.go?s=487:584#L6)
|
||||||
|
``` go
|
||||||
|
type IImage struct {
|
||||||
|
XMLName xml.Name `xml:"itunes:image"`
|
||||||
|
HREF string `xml:"href,attr"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
IImage represents an iTunes image.
|
||||||
|
|
||||||
|
Podcast feeds contain artwork that is a minimum size of
|
||||||
|
1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels,
|
||||||
|
72 dpi, in JPEG or PNG format with appropriate file
|
||||||
|
extensions (.jpg, .png), and in the RGB colorspace. To optimize
|
||||||
|
images for mobile devices, Apple recommends compressing your
|
||||||
|
image files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="Image">type</a> [Image](/src/target/image.go?s=398:656#L3)
|
||||||
|
``` go
|
||||||
|
type Image struct {
|
||||||
|
XMLName xml.Name `xml:"image"`
|
||||||
|
// TODO: is it URL or Link? which is it?
|
||||||
|
URL string `xml:"url"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Width int `xml:"width,omitempty"`
|
||||||
|
Height int `xml:"height,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Image represents an image.
|
||||||
|
|
||||||
|
Podcast feeds contain artwork that is a minimum size of
|
||||||
|
1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels,
|
||||||
|
72 dpi, in JPEG or PNG format with appropriate file
|
||||||
|
extensions (.jpg, .png), and in the RGB colorspace. To optimize
|
||||||
|
images for mobile devices, Apple recommends compressing your
|
||||||
|
image files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="Item">type</a> [Item](/src/target/item.go?s=606:1746#L15)
|
||||||
|
``` go
|
||||||
|
type Item struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
GUID string `xml:"guid"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Author *Author `xml:"-"`
|
||||||
|
AuthorFormatted string `xml:"author,omitempty"`
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Comments string `xml:"comments,omitempty"`
|
||||||
|
Source string `xml:"source,omitempty"`
|
||||||
|
PubDate *time.Time `xml:"-"`
|
||||||
|
PubDateFormatted string `xml:"pubDate,omitempty"`
|
||||||
|
Enclosure *Enclosure
|
||||||
|
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
IAuthor string `xml:"itunes:author,omitempty"`
|
||||||
|
ISubtitle string `xml:"itunes:subtitle,omitempty"`
|
||||||
|
// TODO: CDATA
|
||||||
|
ISummary string `xml:"itunes:summary,omitempty"`
|
||||||
|
IImage *IImage
|
||||||
|
IDuration string `xml:"itunes:duration,omitempty"`
|
||||||
|
IExplicit string `xml:"itunes:explicit,omitempty"`
|
||||||
|
IIsClosedCaptioned string `xml:"itunes:isClosedCaptioned,omitempty"`
|
||||||
|
IOrder string `xml:"itunes:order,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Item represents a single entry in a podcast.
|
||||||
|
|
||||||
|
Article minimal requirements are:
|
||||||
|
- Title
|
||||||
|
- Description
|
||||||
|
- Link
|
||||||
|
|
||||||
|
Audio minimal requirements are:
|
||||||
|
- Title
|
||||||
|
- Description
|
||||||
|
- Enclosure (HREF, Type and Length all required)
|
||||||
|
|
||||||
|
Recommendations:
|
||||||
|
- Setting the minimal fields sets most of other fields, including iTunes.
|
||||||
|
- Use the Published time.Time setting instead of PubDate.
|
||||||
|
- Always set an Enclosure.Length, to be nice to your downloaders.
|
||||||
|
- Use Enclosure.Type instead of setting TypeFormatted for valid extensions.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Item.AddEnclosure">func</a> (\*Item) [AddEnclosure](/src/target/item.go?s=1813:1906#L43)
|
||||||
|
``` go
|
||||||
|
func (i *Item) AddEnclosure(
|
||||||
|
url string, enclosureType EnclosureType, lengthInSeconds int64)
|
||||||
|
```
|
||||||
|
AddEnclosure adds the downloadable asset to the podcast Item.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="Podcast">type</a> [Podcast](/src/target/podcast.go?s=176:1774#L9)
|
||||||
|
``` go
|
||||||
|
type Podcast struct {
|
||||||
|
XMLName xml.Name `xml:"channel"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Cloud string `xml:"cloud,omitempty"`
|
||||||
|
Copyright string `xml:"copyright,omitempty"`
|
||||||
|
Docs string `xml:"docs,omitempty"`
|
||||||
|
Generator string `xml:"generator,omitempty"`
|
||||||
|
Language string `xml:"language,omitempty"`
|
||||||
|
LastBuildDate string `xml:"lastBuildDate,omitempty"`
|
||||||
|
ManagingEditor string `xml:"managingEditor,omitempty"`
|
||||||
|
PubDate string `xml:"pubDate,omitempty"`
|
||||||
|
Rating string `xml:"rating,omitempty"`
|
||||||
|
SkipHours string `xml:"skipHours,omitempty"`
|
||||||
|
SkipDays string `xml:"skipDays,omitempty"`
|
||||||
|
TTL int `xml:"ttl,omitempty"`
|
||||||
|
WebMaster string `xml:"webMaster,omitempty"`
|
||||||
|
Image *Image
|
||||||
|
TextInput *TextInput
|
||||||
|
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
IAuthor string `xml:"itunes:author,omitempty"`
|
||||||
|
ISubtitle string `xml:"itunes:subtitle,omitempty"`
|
||||||
|
// TODO: CDATA
|
||||||
|
ISummary string `xml:"itunes:summary,omitempty"`
|
||||||
|
IBlock string `xml:"itunes:block,omitempty"`
|
||||||
|
IImage *IImage
|
||||||
|
IDuration string `xml:"itunes:duration,omitempty"`
|
||||||
|
IExplicit string `xml:"itunes:explicit,omitempty"`
|
||||||
|
IComplete string `xml:"itunes:complete,omitempty"`
|
||||||
|
INewFeedURL string `xml:"itunes:new-feed-url,omitempty"`
|
||||||
|
IOwner *Author // Author is formatted for itunes as-is
|
||||||
|
ICategories []*ICategory
|
||||||
|
|
||||||
|
Items []*Item
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Podcast represents a podcast.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="New">func</a> [New](/src/target/podcast.go?s=1940:2025#L52)
|
||||||
|
``` go
|
||||||
|
func New(title, link, description string,
|
||||||
|
pubDate, lastBuildDate *time.Time) Podcast
|
||||||
|
```
|
||||||
|
New instantiates a Podcast with required parameters.
|
||||||
|
|
||||||
|
Nil-able fields are optional but recommended as they are formatted
|
||||||
|
to the expected proper formats.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.AddAuthor">func</a> (\*Podcast) [AddAuthor](/src/target/podcast.go?s=2403:2440#L67)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) AddAuthor(a Author)
|
||||||
|
```
|
||||||
|
AddAuthor adds the specified Author to the podcast.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.AddCategory">func</a> (\*Podcast) [AddCategory](/src/target/podcast.go?s=2631:2701#L75)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) AddCategory(category string, subCategories []string)
|
||||||
|
```
|
||||||
|
AddCategory adds the cateories to the Podcast in comma delimited format.
|
||||||
|
|
||||||
|
subCategories are optional.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.AddImage">func</a> (\*Podcast) [AddImage](/src/target/podcast.go?s=3478:3513#L103)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) AddImage(i Image)
|
||||||
|
```
|
||||||
|
AddImage adds the specified Image to the Podcast.
|
||||||
|
|
||||||
|
Podcast feeds contain artwork that is a minimum size of
|
||||||
|
1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels,
|
||||||
|
72 dpi, in JPEG or PNG format with appropriate file
|
||||||
|
extensions (.jpg, .png), and in the RGB colorspace. To optimize
|
||||||
|
images for mobile devices, Apple recommends compressing your
|
||||||
|
image files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.AddItem">func</a> (\*Podcast) [AddItem](/src/target/podcast.go?s=4960:5006#L145)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) AddItem(i Item) (int, error)
|
||||||
|
```
|
||||||
|
AddItem adds the podcast episode. It returns a count of Items added or any
|
||||||
|
errors in validation that may have occurred.
|
||||||
|
|
||||||
|
This method takes the "itunes overrides" approach to populating
|
||||||
|
itunes tags according to the overrides rules in the specification.
|
||||||
|
This not only complies completely with iTunes parsing rules; but, it also
|
||||||
|
displays what is possible to be set on an individial eposide level - if you
|
||||||
|
wish to have more fine grain control over your content.
|
||||||
|
|
||||||
|
This method imposes strict validation of the Item being added to confirm
|
||||||
|
to Podcast and iTunes specifications.
|
||||||
|
|
||||||
|
Article minimal requirements are:
|
||||||
|
* Title
|
||||||
|
* Description
|
||||||
|
* Link
|
||||||
|
|
||||||
|
Audio, Video and Downloads minimal requirements are:
|
||||||
|
* Title
|
||||||
|
* Description
|
||||||
|
* Enclosure (HREF, Type and Length all required)
|
||||||
|
|
||||||
|
The following fields are always overwritten (don't set them):
|
||||||
|
* GUID
|
||||||
|
* PubDateFormatted
|
||||||
|
* AuthorFormatted
|
||||||
|
* Enclosure.TypeFormatted
|
||||||
|
* Enclosure.LengthFormatted
|
||||||
|
|
||||||
|
Recommendations:
|
||||||
|
* Just set the minimal fields: the rest get set for you.
|
||||||
|
* Always set an Enclosure.Length, to be nice to your downloaders.
|
||||||
|
* Follow Apple's best practices to enrich your podcasts:
|
||||||
|
|
||||||
|
|
||||||
|
<a href="https://help.apple.com/itc/podcasts_connect/#/itc2b3780e76">https://help.apple.com/itc/podcasts_connect/#/itc2b3780e76</a>
|
||||||
|
|
||||||
|
* For specifications of itunes tags, see:
|
||||||
|
|
||||||
|
|
||||||
|
<a href="https://help.apple.com/itc/podcasts_connect/#/itcb54353390">https://help.apple.com/itc/podcasts_connect/#/itcb54353390</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.Bytes">func</a> (\*Podcast) [Bytes](/src/target/podcast.go?s=6829:6861#L213)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) Bytes() []byte
|
||||||
|
```
|
||||||
|
Bytes returns an encoded []byte slice.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.Encode">func</a> (\*Podcast) [Encode](/src/target/podcast.go?s=6971:7014#L218)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) Encode(w io.Writer) error
|
||||||
|
```
|
||||||
|
Encode writes the bytes to the io.Writer stream in RSS 2.0 specification.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="Podcast.String">func</a> (\*Podcast) [String](/src/target/podcast.go?s=7091:7124#L223)
|
||||||
|
``` go
|
||||||
|
func (p *Podcast) String() string
|
||||||
|
```
|
||||||
|
String encodes the Podcast state to a string.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a name="TextInput">type</a> [TextInput](/src/target/textinput.go?s=77:290#L1)
|
||||||
|
``` go
|
||||||
|
type TextInput struct {
|
||||||
|
XMLName xml.Name `xml:"textInput"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
TextInput represents text inputs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- - -
|
||||||
|
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
|
|
@ -0,0 +1,12 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// Author represents a named author and email.
|
||||||
|
//
|
||||||
|
// For iTunes compiance, both Name and Email are required.
|
||||||
|
type Author struct {
|
||||||
|
XMLName xml.Name `xml:"itunes:owner"`
|
||||||
|
Name string `xml:"itunes:name"`
|
||||||
|
Email string `xml:"itunes:email"`
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Package `podcast` is an iTunes and RSS 2.0 podcast generator for GoLang that
|
||||||
|
// enforces strict compliance by using its simple interface.
|
||||||
|
//
|
||||||
|
// [![GoDoc](https://godoc.org/github.com/eduncan911/podcast?status.svg)](https://godoc.org/github.com/eduncan911/podcast) [![Build Status](https://travis-ci.org/eduncan911/podcast.svg?branch=master)](https://travis-ci.org/eduncan911/podcast) [![Go Report Card](https://goreportcard.com/badge/github.com/eduncan911/podcast)](https://goreportcard.com/report/github.com/eduncan911/podcast)
|
||||||
|
//
|
||||||
|
// Full documentation with detailed examples located at [![GoDoc](https://godoc.org/github.com/eduncan911/podcast?status.svg)](https://godoc.org/github.com/eduncan911/podcast)
|
||||||
|
//
|
||||||
|
// Usage
|
||||||
|
//
|
||||||
|
// $ go get -u 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.
|
||||||
|
//
|
||||||
|
// See the detailed Examples in the GoDocs for complete usage.
|
||||||
|
//
|
||||||
|
// Extensiblity
|
||||||
|
//
|
||||||
|
// In no way are you 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.
|
||||||
|
//
|
||||||
|
// iTunes specific fields are all prefixed with the letter `I`.
|
||||||
|
//
|
||||||
|
// References
|
||||||
|
//
|
||||||
|
// RSS 2.0: https://cyber.harvard.edu/rss/rss.html
|
||||||
|
//
|
||||||
|
// Podcasts: https://help.apple.com/itc/podcasts_connect/#/itca5b22233
|
||||||
|
package podcast
|
|
@ -0,0 +1,53 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// EnclosureType specifies the type of the enclosure.
|
||||||
|
const (
|
||||||
|
M4A EnclosureType = iota
|
||||||
|
M4V
|
||||||
|
MP4
|
||||||
|
MP3
|
||||||
|
MOV
|
||||||
|
PDF
|
||||||
|
EPUB
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
enclosureDefault = "application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnclosureType specifies the type of the enclosure.
|
||||||
|
type EnclosureType int
|
||||||
|
|
||||||
|
// String returns the MIME type encoding of the specified EnclosureType.
|
||||||
|
func (et EnclosureType) String() string {
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
switch et {
|
||||||
|
case M4A:
|
||||||
|
return "audio/x-m4a"
|
||||||
|
case M4V:
|
||||||
|
return "video/x-m4v"
|
||||||
|
case MP4:
|
||||||
|
return "video/mp4"
|
||||||
|
case MP3:
|
||||||
|
return "audio/mpeg"
|
||||||
|
case MOV:
|
||||||
|
return "video/quicktime"
|
||||||
|
case PDF:
|
||||||
|
return "application/pdf"
|
||||||
|
case EPUB:
|
||||||
|
return "document/x-epub"
|
||||||
|
}
|
||||||
|
return enclosureDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enclosure represents a download enclosure.
|
||||||
|
type Enclosure struct {
|
||||||
|
XMLName xml.Name `xml:"enclosure"`
|
||||||
|
URL string `xml:"url,attr"`
|
||||||
|
Length int64 `xml:"-"`
|
||||||
|
LengthFormatted string `xml:"length,attr"`
|
||||||
|
Type EnclosureType `xml:"-"`
|
||||||
|
TypeFormatted string `xml:"type,attr"`
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package podcast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eduncan911/podcast"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnclosureTypeM4A(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.M4A.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "audio/x-m4a", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypeM4V(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.M4V.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "video/x-m4v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypeMP4(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.MP4.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "video/mp4", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypeMP3(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.MP3.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "audio/mpeg", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypeMOV(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.MOV.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "video/quicktime", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypePDF(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.PDF.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "application/pdf", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypeEPUB(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.EPUB.String()
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "document/x-epub", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnclosureTypeDefault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// act
|
||||||
|
v := podcast.EnclosureType(99)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, "application/octet-stream", v.String())
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
package podcast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eduncan911/podcast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
now := time.Date(2017, time.February, 1, 7, 51, 0, 0, time.Local)
|
||||||
|
|
||||||
|
p := podcast.New(
|
||||||
|
"Sample Podcasts",
|
||||||
|
"http://example.com/",
|
||||||
|
"An example Podcast",
|
||||||
|
&now, &now,
|
||||||
|
)
|
||||||
|
p.ISubtitle = "A simple Podcast"
|
||||||
|
p.AddImage(podcast.Image{URL: "http://example.com/podcast.jpg"})
|
||||||
|
p.AddAuthor(podcast.Author{
|
||||||
|
Name: "Jane Doe",
|
||||||
|
Email: "jane.doe@example.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := int64(0); i < 2; i++ {
|
||||||
|
n := strconv.FormatInt(i, 10)
|
||||||
|
|
||||||
|
item := podcast.Item{
|
||||||
|
Title: "Episode " + n,
|
||||||
|
Description: "Description for Episode " + n,
|
||||||
|
ISubtitle: "A simple episode " + n,
|
||||||
|
PubDate: &now,
|
||||||
|
}
|
||||||
|
item.AddEnclosure(
|
||||||
|
"http://example.com/"+n+".mp3", podcast.MP3, 55*(i+1))
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
os.Stderr.WriteString("error writing to stdout: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
// <rss version="2.0" 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.0.0 (github.com/eduncan911/podcast)</generator>
|
||||||
|
// <language>en-us</language>
|
||||||
|
// <lastBuildDate>Wed, 01 Feb 2017 07:51:00 -0500</lastBuildDate>
|
||||||
|
// <managingEditor>jane.doe@example.com (Jane Doe)</managingEditor>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 07:51:00 -0500</pubDate>
|
||||||
|
// <image>
|
||||||
|
// <url>http://example.com/podcast.jpg</url>
|
||||||
|
// <title></title>
|
||||||
|
// <link></link>
|
||||||
|
// </image>
|
||||||
|
// <itunes:author>jane.doe@example.com (Jane Doe)</itunes:author>
|
||||||
|
// <itunes:subtitle>A simple Podcast</itunes:subtitle>
|
||||||
|
// <itunes:image href="http://example.com/podcast.jpg"></itunes:image>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/0.mp3</guid>
|
||||||
|
// <title>Episode 0</title>
|
||||||
|
// <link>http://example.com/0.mp3</link>
|
||||||
|
// <description>Description for Episode 0</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 07:51:00 -0500</pubDate>
|
||||||
|
// <enclosure url="http://example.com/0.mp3" length="55" type="audio/mpeg"></enclosure>
|
||||||
|
// <itunes:author>jane.doe@example.com (Jane Doe)</itunes:author>
|
||||||
|
// <itunes:subtitle>A simple episode 0</itunes:subtitle>
|
||||||
|
// <itunes:image href="http://example.com/podcast.jpg"></itunes:image>
|
||||||
|
// <itunes:duration>55</itunes:duration>
|
||||||
|
// </item>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/1.mp3</guid>
|
||||||
|
// <title>Episode 1</title>
|
||||||
|
// <link>http://example.com/1.mp3</link>
|
||||||
|
// <description>Description for Episode 1</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 07:51:00 -0500</pubDate>
|
||||||
|
// <enclosure url="http://example.com/1.mp3" length="110" type="audio/mpeg"></enclosure>
|
||||||
|
// <itunes:author>jane.doe@example.com (Jane Doe)</itunes:author>
|
||||||
|
// <itunes:subtitle>A simple episode 1</itunes:subtitle>
|
||||||
|
// <itunes:image href="http://example.com/podcast.jpg"></itunes:image>
|
||||||
|
// <itunes:duration>110</itunes:duration>
|
||||||
|
// </item>
|
||||||
|
// </channel>
|
||||||
|
// </rss>
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_encode() {
|
||||||
|
|
||||||
|
// ResponseWriter example using Podcast.Encode(w io.Writer).
|
||||||
|
//
|
||||||
|
httpHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
p := podcast.New(
|
||||||
|
"eduncan911 Podcasts",
|
||||||
|
"http://eduncan911.com/",
|
||||||
|
"An example Podcast",
|
||||||
|
&pubDate, &pubDate,
|
||||||
|
)
|
||||||
|
for i := int64(0); i < 3; i++ {
|
||||||
|
n := strconv.FormatInt(i, 10)
|
||||||
|
|
||||||
|
item := podcast.Item{
|
||||||
|
Title: "Episode " + n,
|
||||||
|
Link: "http://example.com/" + n + ".mp3",
|
||||||
|
Description: "Description for Episode " + n,
|
||||||
|
PubDate: &pubDate,
|
||||||
|
}
|
||||||
|
if _, err := p.AddItem(item); err != nil {
|
||||||
|
fmt.Println(item.Title, ": error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
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: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.0.0 (github.com/eduncan911/podcast)</generator>
|
||||||
|
// <language>en-us</language>
|
||||||
|
// <lastBuildDate>Wed, 01 Feb 2017 08:21:52 -0500</lastBuildDate>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 08:21:52 -0500</pubDate>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/0.mp3</guid>
|
||||||
|
// <title>Episode 0</title>
|
||||||
|
// <link>http://example.com/0.mp3</link>
|
||||||
|
// <description>Description for Episode 0</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 08:21:52 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/1.mp3</guid>
|
||||||
|
// <title>Episode 1</title>
|
||||||
|
// <link>http://example.com/1.mp3</link>
|
||||||
|
// <description>Description for Episode 1</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 08:21:52 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/2.mp3</guid>
|
||||||
|
// <title>Episode 2</title>
|
||||||
|
// <link>http://example.com/2.mp3</link>
|
||||||
|
// <description>Description for Episode 2</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 08:21:52 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// </channel>
|
||||||
|
// </rss>
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package podcast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eduncan911/podcast"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pubDate = time.Date(2017, time.February, 1, 8, 21, 52, 0, time.Local)
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNew() {
|
||||||
|
ti, l, d := "title", "link", "description"
|
||||||
|
|
||||||
|
p := podcast.New(ti, l, d, &pubDate, &pubDate)
|
||||||
|
|
||||||
|
fmt.Println(p.Title, p.Link, p.Description, p.Language)
|
||||||
|
fmt.Println(p.PubDate, p.LastBuildDate)
|
||||||
|
// Output:
|
||||||
|
// title link description en-us
|
||||||
|
// Wed, 01 Feb 2017 08:21:52 -0500 Wed, 01 Feb 2017 08:21:52 -0500
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePodcast_AddAuthor() {
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
|
||||||
|
p.AddAuthor(podcast.Author{
|
||||||
|
Name: "the name",
|
||||||
|
Email: "me@test.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println(p.ManagingEditor)
|
||||||
|
fmt.Println(p.IAuthor)
|
||||||
|
// Output:
|
||||||
|
// me@test.com (the name)
|
||||||
|
// me@test.com (the name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePodcast_AddCategory() {
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
|
||||||
|
p.AddCategory("Taby", nil)
|
||||||
|
p.AddCategory("North American", []string{"Long Hair", "Short Hair"})
|
||||||
|
p.AddCategory("Simese", nil)
|
||||||
|
|
||||||
|
fmt.Println(len(p.ICategories), len(p.ICategories[1].ICategories))
|
||||||
|
fmt.Println(p.Category)
|
||||||
|
// Output:
|
||||||
|
// 3 2
|
||||||
|
// Taby,North American,Simese
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePodcast_AddImage() {
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
|
||||||
|
p.AddImage(podcast.Image{
|
||||||
|
URL: "http://example.com/image.jpg",
|
||||||
|
})
|
||||||
|
|
||||||
|
if p.Image != nil && p.IImage != nil {
|
||||||
|
fmt.Println(p.Image.URL)
|
||||||
|
fmt.Println(p.IImage.HREF)
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// http://example.com/image.jpg
|
||||||
|
// http://example.com/image.jpg
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePodcast_AddItem() {
|
||||||
|
p := podcast.New("title", "link", "description", &pubDate, &pubDate)
|
||||||
|
p.AddAuthor(podcast.Author{Name: "the name", Email: "me@test.com"})
|
||||||
|
p.AddImage(podcast.Image{URL: "http://example.com/image.jpg"})
|
||||||
|
|
||||||
|
item := podcast.Item{
|
||||||
|
Title: "Episode 1",
|
||||||
|
Description: "Description for Episode 1",
|
||||||
|
ISubtitle: "A simple episode 1",
|
||||||
|
PubDate: &pubDate,
|
||||||
|
}
|
||||||
|
item.AddEnclosure(
|
||||||
|
"http://example.com/1.mp3",
|
||||||
|
podcast.MP3,
|
||||||
|
183,
|
||||||
|
)
|
||||||
|
if _, err := p.AddItem(item); err != nil {
|
||||||
|
fmt.Println("item validation error: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Items) != 1 {
|
||||||
|
fmt.Println("expected 1 item in the collection")
|
||||||
|
}
|
||||||
|
pp := p.Items[0]
|
||||||
|
fmt.Println(
|
||||||
|
pp.GUID, pp.Title, pp.Link, pp.Description, pp.Author,
|
||||||
|
pp.AuthorFormatted, pp.Category, pp.Comments, pp.Source,
|
||||||
|
pp.PubDate, pp.PubDateFormatted, *pp.Enclosure,
|
||||||
|
pp.IAuthor, pp.IDuration, pp.IExplicit, pp.IIsClosedCaptioned,
|
||||||
|
pp.IOrder, pp.ISubtitle, pp.ISummary)
|
||||||
|
// Output:
|
||||||
|
// http://example.com/1.mp3 Episode 1 http://example.com/1.mp3 Description for Episode 1 &{{ } me@test.com (the name)} 2017-02-01 08:21:52 -0500 EST Wed, 01 Feb 2017 08:21:52 -0500 {{ } http://example.com/1.mp3 183 183 audio/mpeg audio/mpeg} me@test.com (the name) 183 A simple episode 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePodcast_Bytes() {
|
||||||
|
pubDate := time.Date(2017, time.February, 1, 9, 11, 0, 0, time.Local)
|
||||||
|
|
||||||
|
p := podcast.New(
|
||||||
|
"eduncan911 Podcasts",
|
||||||
|
"http://eduncan911.com/",
|
||||||
|
"An example Podcast",
|
||||||
|
&pubDate, &pubDate,
|
||||||
|
)
|
||||||
|
for i := int64(0); i < 4; i++ {
|
||||||
|
n := strconv.FormatInt(i, 10)
|
||||||
|
|
||||||
|
item := podcast.Item{
|
||||||
|
Title: "Episode " + n,
|
||||||
|
Link: "http://example.com/" + n + ".mp3",
|
||||||
|
Description: "Description for Episode " + n,
|
||||||
|
PubDate: &pubDate,
|
||||||
|
}
|
||||||
|
if _, err := p.AddItem(item); err != nil {
|
||||||
|
fmt.Println(item.Title, ": error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stdout.Write(p.Bytes())
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
// <rss version="2.0" 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.0.0 (github.com/eduncan911/podcast)</generator>
|
||||||
|
// <language>en-us</language>
|
||||||
|
// <lastBuildDate>Wed, 01 Feb 2017 09:11:00 -0500</lastBuildDate>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 09:11:00 -0500</pubDate>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/0.mp3</guid>
|
||||||
|
// <title>Episode 0</title>
|
||||||
|
// <link>http://example.com/0.mp3</link>
|
||||||
|
// <description>Description for Episode 0</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 09:11:00 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/1.mp3</guid>
|
||||||
|
// <title>Episode 1</title>
|
||||||
|
// <link>http://example.com/1.mp3</link>
|
||||||
|
// <description>Description for Episode 1</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 09:11:00 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/2.mp3</guid>
|
||||||
|
// <title>Episode 2</title>
|
||||||
|
// <link>http://example.com/2.mp3</link>
|
||||||
|
// <description>Description for Episode 2</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 09:11:00 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// <item>
|
||||||
|
// <guid>http://example.com/3.mp3</guid>
|
||||||
|
// <title>Episode 3</title>
|
||||||
|
// <link>http://example.com/3.mp3</link>
|
||||||
|
// <description>Description for Episode 3</description>
|
||||||
|
// <pubDate>Wed, 01 Feb 2017 09:11:00 -0500</pubDate>
|
||||||
|
// </item>
|
||||||
|
// </channel>
|
||||||
|
// </rss>
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// Image represents an image.
|
||||||
|
//
|
||||||
|
// Podcast feeds contain artwork that is a minimum size of
|
||||||
|
// 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels,
|
||||||
|
// 72 dpi, in JPEG or PNG format with appropriate file
|
||||||
|
// extensions (.jpg, .png), and in the RGB colorspace. To optimize
|
||||||
|
// images for mobile devices, Apple recommends compressing your
|
||||||
|
// image files.
|
||||||
|
type Image struct {
|
||||||
|
XMLName xml.Name `xml:"image"`
|
||||||
|
// TODO: is it URL or Link? which is it?
|
||||||
|
URL string `xml:"url"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Width int `xml:"width,omitempty"`
|
||||||
|
Height int `xml:"height,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Item represents a single entry in a podcast.
|
||||||
|
//
|
||||||
|
// Article minimal requirements are:
|
||||||
|
// - Title
|
||||||
|
// - Description
|
||||||
|
// - Link
|
||||||
|
//
|
||||||
|
// Audio minimal requirements are:
|
||||||
|
// - Title
|
||||||
|
// - Description
|
||||||
|
// - Enclosure (HREF, Type and Length all required)
|
||||||
|
//
|
||||||
|
// Recommendations:
|
||||||
|
// - Setting the minimal fields sets most of other fields, including iTunes.
|
||||||
|
// - Use the Published time.Time setting instead of PubDate.
|
||||||
|
// - Always set an Enclosure.Length, to be nice to your downloaders.
|
||||||
|
// - Use Enclosure.Type instead of setting TypeFormatted for valid extensions.
|
||||||
|
type Item struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
GUID string `xml:"guid"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Author *Author `xml:"-"`
|
||||||
|
AuthorFormatted string `xml:"author,omitempty"`
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Comments string `xml:"comments,omitempty"`
|
||||||
|
Source string `xml:"source,omitempty"`
|
||||||
|
PubDate *time.Time `xml:"-"`
|
||||||
|
PubDateFormatted string `xml:"pubDate,omitempty"`
|
||||||
|
Enclosure *Enclosure
|
||||||
|
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
IAuthor string `xml:"itunes:author,omitempty"`
|
||||||
|
ISubtitle string `xml:"itunes:subtitle,omitempty"`
|
||||||
|
// TODO: CDATA
|
||||||
|
ISummary string `xml:"itunes:summary,omitempty"`
|
||||||
|
IImage *IImage
|
||||||
|
IDuration string `xml:"itunes:duration,omitempty"`
|
||||||
|
IExplicit string `xml:"itunes:explicit,omitempty"`
|
||||||
|
IIsClosedCaptioned string `xml:"itunes:isClosedCaptioned,omitempty"`
|
||||||
|
IOrder string `xml:"itunes:order,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEnclosure adds the downloadable asset to the podcast Item.
|
||||||
|
func (i *Item) AddEnclosure(
|
||||||
|
url string, enclosureType EnclosureType, lengthInSeconds int64) {
|
||||||
|
i.Enclosure = &Enclosure{
|
||||||
|
URL: url,
|
||||||
|
Type: enclosureType,
|
||||||
|
Length: lengthInSeconds,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// Speicfication: https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
//
|
||||||
|
|
||||||
|
// IImage represents an iTunes image.
|
||||||
|
//
|
||||||
|
// Podcast feeds contain artwork that is a minimum size of
|
||||||
|
// 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels,
|
||||||
|
// 72 dpi, in JPEG or PNG format with appropriate file
|
||||||
|
// extensions (.jpg, .png), and in the RGB colorspace. To optimize
|
||||||
|
// images for mobile devices, Apple recommends compressing your
|
||||||
|
// image files.
|
||||||
|
type IImage struct {
|
||||||
|
XMLName xml.Name `xml:"itunes:image"`
|
||||||
|
HREF string `xml:"href,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICategory is a 2-tier classification system for iTunes.
|
||||||
|
type ICategory struct {
|
||||||
|
XMLName xml.Name `xml:"itunes:category"`
|
||||||
|
Text string `xml:"text,attr"`
|
||||||
|
ICategories []*ICategory
|
||||||
|
}
|
|
@ -0,0 +1,294 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pVersion = "1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Podcast represents a podcast.
|
||||||
|
type Podcast struct {
|
||||||
|
XMLName xml.Name `xml:"channel"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Cloud string `xml:"cloud,omitempty"`
|
||||||
|
Copyright string `xml:"copyright,omitempty"`
|
||||||
|
Docs string `xml:"docs,omitempty"`
|
||||||
|
Generator string `xml:"generator,omitempty"`
|
||||||
|
Language string `xml:"language,omitempty"`
|
||||||
|
LastBuildDate string `xml:"lastBuildDate,omitempty"`
|
||||||
|
ManagingEditor string `xml:"managingEditor,omitempty"`
|
||||||
|
PubDate string `xml:"pubDate,omitempty"`
|
||||||
|
Rating string `xml:"rating,omitempty"`
|
||||||
|
SkipHours string `xml:"skipHours,omitempty"`
|
||||||
|
SkipDays string `xml:"skipDays,omitempty"`
|
||||||
|
TTL int `xml:"ttl,omitempty"`
|
||||||
|
WebMaster string `xml:"webMaster,omitempty"`
|
||||||
|
Image *Image
|
||||||
|
TextInput *TextInput
|
||||||
|
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
IAuthor string `xml:"itunes:author,omitempty"`
|
||||||
|
ISubtitle string `xml:"itunes:subtitle,omitempty"`
|
||||||
|
// TODO: CDATA
|
||||||
|
ISummary string `xml:"itunes:summary,omitempty"`
|
||||||
|
IBlock string `xml:"itunes:block,omitempty"`
|
||||||
|
IImage *IImage
|
||||||
|
IDuration string `xml:"itunes:duration,omitempty"`
|
||||||
|
IExplicit string `xml:"itunes:explicit,omitempty"`
|
||||||
|
IComplete string `xml:"itunes:complete,omitempty"`
|
||||||
|
INewFeedURL string `xml:"itunes:new-feed-url,omitempty"`
|
||||||
|
IOwner *Author // Author is formatted for itunes as-is
|
||||||
|
ICategories []*ICategory
|
||||||
|
|
||||||
|
Items []*Item
|
||||||
|
}
|
||||||
|
|
||||||
|
// New instantiates a Podcast with required parameters.
|
||||||
|
//
|
||||||
|
// Nil-able fields are optional but recommended as they are formatted
|
||||||
|
// to the expected proper formats.
|
||||||
|
func New(title, link, description string,
|
||||||
|
pubDate, lastBuildDate *time.Time) Podcast {
|
||||||
|
p := Podcast{
|
||||||
|
Title: title,
|
||||||
|
Link: link,
|
||||||
|
Description: description,
|
||||||
|
Generator: fmt.Sprintf("go podcast v%s (github.com/eduncan911/podcast)", pVersion),
|
||||||
|
PubDate: parseDateRFC1123Z(pubDate),
|
||||||
|
LastBuildDate: parseDateRFC1123Z(lastBuildDate),
|
||||||
|
Language: "en-us",
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAuthor adds the specified Author to the podcast.
|
||||||
|
func (p *Podcast) AddAuthor(a Author) {
|
||||||
|
p.ManagingEditor = parseAuthorNameEmail(&a)
|
||||||
|
p.IAuthor = p.ManagingEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCategory adds the cateories to the Podcast in comma delimited format.
|
||||||
|
//
|
||||||
|
// subCategories are optional.
|
||||||
|
func (p *Podcast) AddCategory(category string, subCategories []string) {
|
||||||
|
if len(category) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSS 2.0 Category only supports 1-tier
|
||||||
|
if len(p.Category) > 0 {
|
||||||
|
p.Category = p.Category + "," + category
|
||||||
|
} else {
|
||||||
|
p.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
icat := ICategory{Text: category}
|
||||||
|
for _, c := range subCategories {
|
||||||
|
icat2 := ICategory{Text: c}
|
||||||
|
icat.ICategories = append(icat.ICategories, &icat2)
|
||||||
|
}
|
||||||
|
p.ICategories = append(p.ICategories, &icat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddImage adds the specified Image to the Podcast.
|
||||||
|
//
|
||||||
|
// Podcast feeds contain artwork that is a minimum size of
|
||||||
|
// 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels,
|
||||||
|
// 72 dpi, in JPEG or PNG format with appropriate file
|
||||||
|
// extensions (.jpg, .png), and in the RGB colorspace. To optimize
|
||||||
|
// images for mobile devices, Apple recommends compressing your
|
||||||
|
// image files.
|
||||||
|
func (p *Podcast) AddImage(i Image) {
|
||||||
|
p.Image = &i
|
||||||
|
p.IImage = &IImage{HREF: p.Image.URL}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddItem adds the podcast episode. It returns a count of Items added or any
|
||||||
|
// errors in validation that may have occurred.
|
||||||
|
//
|
||||||
|
// This method takes the "itunes overrides" approach to populating
|
||||||
|
// itunes tags according to the overrides rules in the specification.
|
||||||
|
// This not only complies completely with iTunes parsing rules; but, it also
|
||||||
|
// displays what is possible to be set on an individial eposide level - if you
|
||||||
|
// wish to have more fine grain control over your content.
|
||||||
|
//
|
||||||
|
// This method imposes strict validation of the Item being added to confirm
|
||||||
|
// to Podcast and iTunes specifications.
|
||||||
|
//
|
||||||
|
// Article minimal requirements are:
|
||||||
|
// * Title
|
||||||
|
// * Description
|
||||||
|
// * Link
|
||||||
|
//
|
||||||
|
// Audio, Video and Downloads minimal requirements are:
|
||||||
|
// * Title
|
||||||
|
// * Description
|
||||||
|
// * Enclosure (HREF, Type and Length all required)
|
||||||
|
//
|
||||||
|
// The following fields are always overwritten (don't set them):
|
||||||
|
// * GUID
|
||||||
|
// * PubDateFormatted
|
||||||
|
// * AuthorFormatted
|
||||||
|
// * Enclosure.TypeFormatted
|
||||||
|
// * Enclosure.LengthFormatted
|
||||||
|
//
|
||||||
|
// Recommendations:
|
||||||
|
// * Just set the minimal fields: the rest get set for you.
|
||||||
|
// * Always set an Enclosure.Length, to be nice to your downloaders.
|
||||||
|
// * Follow Apple's best practices to enrich your podcasts:
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itc2b3780e76
|
||||||
|
// * For specifications of itunes tags, see:
|
||||||
|
// https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||||
|
//
|
||||||
|
func (p *Podcast) AddItem(i Item) (int, error) {
|
||||||
|
// initial guards for required fields
|
||||||
|
if len(i.Title) == 0 || len(i.Description) == 0 {
|
||||||
|
return len(p.Items), errors.New("Title and Description are reuired")
|
||||||
|
}
|
||||||
|
if i.Enclosure != nil {
|
||||||
|
if len(i.Enclosure.URL) == 0 {
|
||||||
|
return len(p.Items),
|
||||||
|
errors.New(i.Title + ": Enclosure.URL is required")
|
||||||
|
}
|
||||||
|
if i.Enclosure.Type.String() == enclosureDefault {
|
||||||
|
return len(p.Items),
|
||||||
|
errors.New(i.Title + ": Enclosure.Type is required")
|
||||||
|
}
|
||||||
|
} else if len(i.Link) == 0 {
|
||||||
|
return len(p.Items),
|
||||||
|
errors.New(i.Title + ": Link is required when not using Enclosure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// corrective actions and overrides
|
||||||
|
//
|
||||||
|
i.PubDateFormatted = parseDateRFC1123Z(i.PubDate)
|
||||||
|
i.AuthorFormatted = parseAuthorNameEmail(i.Author)
|
||||||
|
if i.Enclosure != nil {
|
||||||
|
i.GUID = i.Enclosure.URL // yep, GUID is the Permlink URL
|
||||||
|
|
||||||
|
if i.Enclosure.Length < 0 {
|
||||||
|
i.Enclosure.Length = 0
|
||||||
|
}
|
||||||
|
i.Enclosure.LengthFormatted = strconv.FormatInt(i.Enclosure.Length, 10)
|
||||||
|
i.Enclosure.TypeFormatted = i.Enclosure.Type.String()
|
||||||
|
|
||||||
|
// allow Link to be set for article references to Downloads,
|
||||||
|
// otherwise set it to the enclosurer's URL.
|
||||||
|
if len(i.Link) == 0 {
|
||||||
|
i.Link = i.Enclosure.URL
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i.GUID = i.Link // yep, GUID is the Permlink URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// iTunes it
|
||||||
|
//
|
||||||
|
if len(i.IAuthor) == 0 {
|
||||||
|
if i.Author != nil {
|
||||||
|
i.IAuthor = i.Author.Email
|
||||||
|
} else if len(p.IAuthor) != 0 {
|
||||||
|
i.Author = &Author{Email: p.IAuthor}
|
||||||
|
i.IAuthor = p.IAuthor
|
||||||
|
} else if len(p.ManagingEditor) != 0 {
|
||||||
|
i.Author = &Author{Email: p.ManagingEditor}
|
||||||
|
i.IAuthor = p.ManagingEditor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.IImage == nil {
|
||||||
|
if p.Image != nil {
|
||||||
|
i.IImage = &IImage{HREF: p.Image.URL}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.Enclosure != nil {
|
||||||
|
i.IDuration = parseDuration(i.Enclosure.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Items = append(p.Items, &i)
|
||||||
|
return len(p.Items), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns an encoded []byte slice.
|
||||||
|
func (p *Podcast) Bytes() []byte {
|
||||||
|
return []byte(p.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode writes the bytes to the io.Writer stream in RSS 2.0 specification.
|
||||||
|
func (p *Podcast) Encode(w io.Writer) error {
|
||||||
|
return encode(w, *p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String encodes the Podcast state to a string.
|
||||||
|
func (p *Podcast) String() string {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
if err := encode(b, *p); err != nil {
|
||||||
|
return "String: podcast.write returned the error: " + err.Error()
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type podcastWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"rss"`
|
||||||
|
Version string `xml:"version,attr"`
|
||||||
|
XMLNS string `xml:"xmlns:itunes,attr"`
|
||||||
|
Channel *Podcast
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode writes the bytes to the io.Writer in RSS 2.0 specification.
|
||||||
|
func encode(w io.Writer, p Podcast) error {
|
||||||
|
e := xml.NewEncoder(w)
|
||||||
|
e.Indent("", " ")
|
||||||
|
|
||||||
|
// <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
w.Write([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"))
|
||||||
|
// <rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
||||||
|
wrapped := podcastWrapper{
|
||||||
|
XMLNS: "http://www.itunes.com/dtds/podcast-1.0.dtd",
|
||||||
|
Version: "2.0",
|
||||||
|
Channel: &p,
|
||||||
|
}
|
||||||
|
if err := e.Encode(wrapped); err != nil {
|
||||||
|
return errors.Wrap(err, "podcast.encode: Encode returned error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDateRFC1123Z(t *time.Time) string {
|
||||||
|
if t != nil && !t.IsZero() {
|
||||||
|
return t.Format(time.RFC1123Z)
|
||||||
|
}
|
||||||
|
return time.Now().UTC().Format(time.RFC1123Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAuthorNameEmail(a *Author) string {
|
||||||
|
var author string
|
||||||
|
if a != nil {
|
||||||
|
author = a.Email
|
||||||
|
if len(a.Name) > 0 {
|
||||||
|
author = fmt.Sprintf("%s (%s)", a.Email, a.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return author
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDuration(duration int64) string {
|
||||||
|
// TODO: parse the output into iTunes nicely formatted version.
|
||||||
|
//
|
||||||
|
// iTunes supports the following:
|
||||||
|
// HH:MM:SS
|
||||||
|
// H:MM:SS
|
||||||
|
// MM:SS
|
||||||
|
// M:SS
|
||||||
|
return strconv.FormatInt(duration, 10)
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
package podcast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eduncan911/podcast"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewNils(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
ti, l, d := "title", "link", "description"
|
||||||
|
|
||||||
|
// act
|
||||||
|
p := podcast.New(ti, l, d, nil, nil)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, ti, p.Title)
|
||||||
|
assert.EqualValues(t, l, p.Link)
|
||||||
|
assert.EqualValues(t, d, p.Description)
|
||||||
|
assert.True(t, time.Now().UTC().Format(time.RFC1123Z) >= p.PubDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddCategoryEmpty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
|
||||||
|
// act
|
||||||
|
p.AddCategory("", nil)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.Len(t, p.ICategories, 0)
|
||||||
|
assert.Len(t, p.Category, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemEmptyTitleDescription(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{}
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 0, added)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Title")
|
||||||
|
assert.Contains(t, err.Error(), "Description")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemEnclosureURLEmpty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
i.AddEnclosure("", podcast.MP3, 1)
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 0, added)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Enclosure.URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemEnclosureTypeEmpty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
i.AddEnclosure("http://example.com/1.mp3", 99, 1)
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 0, added)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Enclosure.Type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemLinkEmpty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 0, added)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Link is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemEnclosureLengthMin(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
i.AddEnclosure("http://example.com/1.mp3", podcast.MP3, -1)
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, 0, p.Items[0].Enclosure.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemEnclosureNoLinkOverride(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
i.AddEnclosure("http://example.com/1.mp3", podcast.MP3, -1)
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, i.Enclosure.URL, p.Items[0].Link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemEnclosureLinkPresentNoOverride(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
theLink := "http://someotherurl.com/story.html"
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
i.Link = theLink
|
||||||
|
i.AddEnclosure("http://example.com/1.mp3", podcast.MP3, -1)
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, theLink, p.Items[0].Link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemNoEnclosureGUIDValid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
theLink := "http://someotherurl.com/story.html"
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc"}
|
||||||
|
i.Link = theLink
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, theLink, p.Items[0].GUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemAuthor(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
theAuthor := podcast.Author{Name: "Jane Doe", Email: "me@janedoe.com"}
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc", Link: "http://a.co/"}
|
||||||
|
i.Author = &theAuthor
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, &theAuthor, p.Items[0].Author)
|
||||||
|
assert.EqualValues(t, theAuthor.Email, p.Items[0].IAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemRootManagingEditorSetsAuthorIAuthor(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
theAuthor := "me@janedoe.com"
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
p.ManagingEditor = theAuthor
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc", Link: "http://a.co/"}
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, theAuthor, p.Items[0].Author.Email)
|
||||||
|
assert.EqualValues(t, theAuthor, p.Items[0].IAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItemRootIAuthorSetsAuthorIAuthor(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
p := podcast.New("title", "link", "description", nil, nil)
|
||||||
|
p.IAuthor = "me@janedoe.com"
|
||||||
|
i := podcast.Item{Title: "title", Description: "desc", Link: "http://a.co/"}
|
||||||
|
|
||||||
|
// act
|
||||||
|
added, err := p.AddItem(i)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.EqualValues(t, 1, added)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Items, 1)
|
||||||
|
assert.EqualValues(t, "me@janedoe.com", p.Items[0].Author.Email)
|
||||||
|
assert.EqualValues(t, "me@janedoe.com", p.Items[0].IAuthor)
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package podcast
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// TextInput represents text inputs.
|
||||||
|
type TextInput struct {
|
||||||
|
XMLName xml.Name `xml:"textInput"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
}
|
Loading…
Reference in New Issue