diff --git a/README.md b/README.md index 4cc9791..b47a051 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. @@ -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. diff --git a/cmd/feedupdater/main.go b/cmd/feedupdater/main.go index c16aca4..4f26e8c 100644 --- a/cmd/feedupdater/main.go +++ b/cmd/feedupdater/main.go @@ -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) } diff --git a/go.mod b/go.mod index 696b1be..828dace 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 3849392..77031d0 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/pkg/appu/feed.go b/pkg/appu/feed.go index 288dc1f..e400ac6 100644 --- a/pkg/appu/feed.go +++ b/pkg/appu/feed.go @@ -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. @@ -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()) + } }