Skip to content

Commit

Permalink
Use RSS feed libraries to mangle the RSS Feed
Browse files Browse the repository at this point in the history
Using XML libraries to manipulate RSS feed is too complicated.
This implementation uses
[gofeed](https://github.com/mmcdole/gofeed) to read current
feed, and [podcast](https://github.com/eduncan911/podcast) to
add the new episode and write it to stdout.

This fixes the bug on newlines in episode description (#44),
while making the code easier to understand.
  • Loading branch information
ifosch committed Jan 8, 2022
1 parent e90a5de commit 6224e71
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 29 deletions.
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# appu

Appu is a toolkit for podcast edition and publishing.
**A**utomatic **P**odcast **PU**blisher, aka appu, is a toolkit for podcast edition and publishing.

## Rationale

While running the [Entre Dev Y Ops podcast](https://www.entredevyops.es), the authors found interesting to start building a set of tools to make this easier. We hope this might help anyone else.
While running the [Entre Dev y Ops podcast](https://www.entredevyops.es), the authors found interesting to start building a set of tools to make this easier. We hope this might help anyone else.

Currently we start preparing every episode by writing a simple script we store in a shared Drive folder.

Expand All @@ -20,9 +20,8 @@ Finally, using the tools here, and our own podcast configuration file, we have m

You'll need the following:
- Docker, for Windows and Mac, you should use Docker Desktop.
- Publishing infrastructure based on an online storage, and a CDN with invalidation features.
Currently only AWS S3 and CloudFront are supported.
You'll need credentials for uploading the episodes' audio files and the RSS feed and then invalidating them on the CDN.
- Publishing infrastructure based on an online storage, and a CDN with invalidation features (currently only AWS S3 and CloudFront are supported.)
- Valid credentials for uploading the episodes' audio files and the RSS feed and then invalidating them on the CDN.

We recommend to use some secure online storage for your podcast configuration and specific details.
It's also interesting for sharing the publishing process within teams.
Expand Down Expand Up @@ -58,16 +57,13 @@ docker build -t ghcr.io/edyo/appu:local appu/.
#### Download and update the feed

```
./feedupdater http://my.podcast.com/podcast/feed.xml mypodcast-XX.yaml
./feedupdater http://my.podcast.com/podcast/feed.xml mypodcast-XX.yaml > new_feed.xml
```

Open the original feed downloaded, `feed.xml` in this case, to avoid editing all entries, and add the last `item` element from the `new_feed.xml`.
Then replace the new lines in the `description` tag's text for the last `item` with the corresponding HTML entity, i.e. `
`.

#### Upload the new feed

```
./uploader feed.xml feed.xml mypodcast-XX.yaml
./uploader new_feed.xml feed.xml mypodcast-XX.yaml
```

First argument is the name of the XML file with the new episode entry. The second argument is the bucket key to upload the file to. The Third argument is the episode configuration file.
Expand Down
16 changes: 5 additions & 11 deletions cmd/feedupdater/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,15 @@ func main() {
log.Fatalf("Error downloading feed %s: %v", feedURL, err)
}

doc, err := appu.ReadXML(feedFileName)
feed, err := appu.ReadXML(feedFileName)
if err != nil {
log.Fatalf("Error parsing feed: %v", err)
}

channel := doc.FindElement("./rss/channel")
if channel != nil {
newEpisodeTag, err := appu.CreateFeedItem(cfg)
if err != nil {
log.Fatalf("Error creating new episode tag: %v", err)
}
channel.AddChild(newEpisodeTag)
} else {
log.Fatalf("Error: could not find channel tagin feed XML")
err = appu.AddNewEpisode(cfg, feed)
if err != nil {
log.Fatalf("Error adding new episode: %v", err)
}

appu.WriteXML(doc)
appu.WriteXML(feed)
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ require (
github.com/antchfx/xpath v1.2.0
github.com/containerd/containerd v1.5.5 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/eduncan911/podcast v1.4.2
github.com/gorilla/mux v1.8.0 // indirect
github.com/ifosch/stationery v0.1.2
github.com/mmcdole/gofeed v1.1.3
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
Expand All @@ -27,6 +29,8 @@ require (
require (
cloud.google.com/go v0.93.3 // indirect
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/PuerkitoBio/goquery v1.5.1 // indirect
github.com/andybalholm/cascadia v1.1.0 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/go-ini/ini v1.25.4 // indirect
Expand All @@ -35,6 +39,10 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
Expand All @@ -88,6 +90,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antchfx/htmlquery v1.2.4 h1:qLteofCMe/KGovBI6SQgmou2QNyedFUW+pE+BpeZ494=
github.com/antchfx/htmlquery v1.2.4/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc=
github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8=
Expand Down Expand Up @@ -267,6 +271,8 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eduncan911/podcast v1.4.2 h1:S+fsUlbR2ULFou2Mc52G/MZI8JVJHedbxLQnoA+MY/w=
github.com/eduncan911/podcast v1.4.2/go.mod h1:mSxiK1z5KeNO0YFaQ3ElJlUZbbDV9dA7R9c1coeeXkc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
Expand Down Expand Up @@ -441,6 +447,7 @@ github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
Expand Down Expand Up @@ -481,6 +488,10 @@ github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
Expand All @@ -489,8 +500,10 @@ github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/f
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
Expand Down Expand Up @@ -640,6 +653,7 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
Expand Down Expand Up @@ -727,6 +741,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
95 changes: 87 additions & 8 deletions pkg/appu/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,96 @@ package appu

import (
"fmt"
"os"
"strconv"
"time"

"github.com/beevik/etree"
"github.com/eduncan911/podcast"
"github.com/mmcdole/gofeed"
)

// ReadXML loads XML from a file into an `etree.Document` for processing.
func ReadXML(feedFileName string) (*etree.Document, error) {
doc := etree.NewDocument()
err := doc.ReadFromFile(feedFileName)
// AddNewEpisode creates a new episode from `cfg` and adds it to `feed`.
func AddNewEpisode(cfg *Config, feed *podcast.Podcast) error {
description := cfg.Summary + "\n"
for _, link := range cfg.Links {
description += link + "\n"
}
newItem := podcast.Item{
Title: cfg.Title,
Description: description,
PubDate: &cfg.PubDate,
}
length, err := GetEpisodeLength(cfg.EpisodeURL)
if err != nil {
return err
}
newItem.AddEnclosure(cfg.EpisodeURL, podcast.MP3, int64(length))

if _, err := feed.AddItem(newItem); err != nil {
return err
}

return nil
}

// ReadXML loads XML from a file into a `podcast.Podcast` for processing.
func ReadXML(feedFileName string) (*podcast.Podcast, error) {
file, err := os.Open(feedFileName)
if err != nil {
return nil, err
}
defer file.Close()

return doc, err
fp := gofeed.NewParser()
feed, err := fp.Parse(file)
if err != nil {
return nil, err
}

p := podcast.New(
feed.Title,
feed.Link,
feed.Description,
feed.PublishedParsed,
feed.UpdatedParsed,
)

p.AddAtomLink(feed.Link)
p.ISubtitle = feed.ITunesExt.Subtitle
p.AddAuthor(feed.ITunesExt.Author, feed.ITunesExt.Owner.Email)
p.AddImage(feed.ITunesExt.Image)
p.AddSummary(feed.ITunesExt.Summary)
for _, category := range feed.ITunesExt.Categories {
p.AddCategory(category.Text, nil)
}
p.IOwner = &podcast.Author{
Name: feed.ITunesExt.Owner.Name,
Email: feed.ITunesExt.Owner.Email,
}
p.IExplicit = feed.ITunesExt.Explicit
p.Language = feed.Language
p.Copyright = feed.Copyright

for _, item := range feed.Items {
podcastItem := podcast.Item{
Title: item.Title,
Description: item.Description,
PubDate: item.PublishedParsed,
}
for _, itemEnclosure := range item.Enclosures {
length, err := strconv.ParseInt(itemEnclosure.Length, 10, 64)
if err != nil {
return nil, err
}
podcastItem.AddEnclosure(itemEnclosure.URL, podcast.MP3, length)
}
if _, err := p.AddItem(podcastItem); err != nil {
return nil, err
}
}

return &p, nil
}

// CreateFeedItem creates a new feed's Item from the Config information.
Expand Down Expand Up @@ -62,7 +138,10 @@ func CreateFeedItem(cfg *Config) (*etree.Element, error) {
}

// WriteXML creates a new file on disk with the appropriate XML contents from `doc`.
func WriteXML(doc *etree.Document) {
doc.Indent(2)
doc.WriteToFile("new_feed.xml")
func WriteXML(p *podcast.Podcast) {

// Podcast.Encode writes to an io.Writer
if err := p.Encode(os.Stdout); err != nil {
fmt.Println("error writing to stdout:", err.Error())
}
}

0 comments on commit 6224e71

Please sign in to comment.