Skip to content

Commit

Permalink
feat: adding authors and narrators podcast feeds (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
CallumKerson committed Apr 13, 2023
1 parent 51cafb2 commit 68984ba
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 36 deletions.
1 change: 1 addition & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ tasks:
- task: go:mod-tidy
- task: go:fmt
- task: go:isort
- task: go:lint
- pre-commit run -a

build:
Expand Down
20 changes: 18 additions & 2 deletions cmd/athenaeum/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,33 @@ func TestRootCommand(t *testing.T) {
path: "/podcast/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "application/xml; charset=utf-8",
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "expected.rss", host),
},
{
name: "sci-fi feed",
path: "/podcast/genre/lgbt+/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "application/xml; charset=utf-8",
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "lgbt.rss", host),
},
{
name: "author feed",
path: "/podcast/authors/Ursula%20K.%20Le%20Guin/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "le_guin.rss", host),
},
{
name: "narrator feed",
path: "/podcast/narrators/Emily%20Woo%20Zeller/feed.rss",
method: "GET",
expectedStatus: 200,
expectedContentType: "text/xml; charset=utf-8",
expectedBody: getExpectedFeed(t, "woo_zeller.rss", host),
},
}

for _, testCase := range tests {
Expand Down
23 changes: 23 additions & 0 deletions cmd/athenaeum/testdata/le_guin.rss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>Ursula K. Le Guin</title>
<link>{{.}}</link>
<copyright></copyright>
<language>EN</language>
<description>Audiobooks by Ursula K. Le Guin</description>
<itunes:block>yes</itunes:block>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="{{.}}/static/itunes_image.jpg"></itunes:image>
<item>
<title>A Wizard of Earthsea</title>
<guid>{{.}}/media/Ursula%20K%20Le%20Guin/Earthsea/1%20A%20Wizard%20of%20Earthsea/A%20Wizard%20of%20Earthsea.m4b</guid>
<pubDate>Thu, 01 Jan 1970 08:00:00 +0000</pubDate>
<description><![CDATA[A Wizard of Earthsea by Ursula K. Le Guin]]></description>
<content:encoded><![CDATA[<h1>A Wizard of Earthsea</h1><h2>By Ursula K. Le Guin</h2><h4>Earthsea Book 1</h4><h4>Narrated by Kobna Holdbrook-Smith</h4><p>Ged, the greatest sorcerer in all Earthsea, was called Sparrowhawk in his reckless youth.</p><p>Hungry for power and knowledge, Sparrowhawk tampered with long-held secrets and loosed a terrible shadow upon the world. This is the tale of his testing, how he mastered the mighty words of power, tamed an ancient dragon, and crossed death's threshold to restore the balance.</p>]]></content:encoded>
<itunes:duration>0:04</itunes:duration>
<itunes:subtitle>Ursula K. Le Guin</itunes:subtitle>
<enclosure url="{{.}}/media/Ursula%20K%20Le%20Guin/Earthsea/1%20A%20Wizard%20of%20Earthsea/A%20Wizard%20of%20Earthsea.m4b" length="145565" type="audio/mp4a-latm"></enclosure>
</item>
</channel>
</rss>
23 changes: 23 additions & 0 deletions cmd/athenaeum/testdata/woo_zeller.rss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>Emily Woo Zeller</title>
<link>{{.}}</link>
<copyright></copyright>
<language>EN</language>
<description>Audiobooks Narrated by Emily Woo Zeller</description>
<itunes:block>yes</itunes:block>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="{{.}}/static/itunes_image.jpg"></itunes:image>
<item>
<title>This Is How You Lose the Time War</title>
<guid>{{.}}/media/Amal%20El-Mohtar%20and%20Max%20Gladstone/This%20Is%20How%20You%20Lose%20the%20Time%20War/This%20Is%20How%20You%20Lose%20the%20Time%20War.m4b</guid>
<pubDate>Tue, 16 Jul 2019 08:00:00 +0000</pubDate>
<description><![CDATA[This Is How You Lose the Time War by Amal El-Mohtar & Max Gladstone]]></description>
<content:encoded><![CDATA[<h1>This Is How You Lose the Time War</h1><h2>By Amal El-Mohtar & Max Gladstone</h2><h4>Narrated by Cynthia Farrell & Emily Woo Zeller</h4><p>Among the ashes of a dying world, an agent of the Commandant finds a letter. It reads: Burn before reading.</p><p>Thus begins an unlikely correspondence between two rival agents hellbent on securing the best possible future for their warring factions. Now, what began as a taunt, a battlefield boast, grows into something more. Something epic. Something romantic. Something that could change the past and the future.</p><p>Except the discovery of their bond would mean death for each of them. There's still a war going on, after all. And someone has to win that war. That's how war works. Right?</p>]]></content:encoded>
<itunes:duration>0:04</itunes:duration>
<itunes:subtitle>Amal El-Mohtar &amp; Max Gladstone</itunes:subtitle>
<enclosure url="{{.}}/media/Amal%20El-Mohtar%20and%20Max%20Gladstone/This%20Is%20How%20You%20Lose%20the%20Time%20War/This%20Is%20How%20You%20Lose%20the%20Time%20War.m4b" length="145608" type="audio/mp4a-latm"></enclosure>
</item>
</channel>
</rss>
9 changes: 9 additions & 0 deletions internal/audiobooks/service/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ func GenreFilter(genre audiobooks.Genre) Filter {
}
}

func NarratorFilter(name string) Filter {
return func(a *audiobooks.Audiobook) bool {
if a != nil && contains(a.Narrators, name) {
return true
}
return false
}
}

func contains[K comparable](slice []K, item K) bool {
for _, v := range slice {
if v == item {
Expand Down
2 changes: 2 additions & 0 deletions internal/audiobooks/service/filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ func TestFilers(t *testing.T) {
{name: "not match author", filter: AuthorFilter("Max Gladstone"), book: testbooks.Audiobooks[1], expectedMatch: false},
{name: "match genre", filter: GenreFilter(audiobooks.SciFi), book: testbooks.Audiobooks[0], expectedMatch: true},
{name: "not match genre", filter: GenreFilter(audiobooks.SciFi), book: testbooks.Audiobooks[1], expectedMatch: false},
{name: "match narrator", filter: NarratorFilter("Emily Woo Zeller"), book: testbooks.Audiobooks[0], expectedMatch: true},
{name: "not match narrator", filter: NarratorFilter("Emily Woo Zeller"), book: testbooks.Audiobooks[1], expectedMatch: false},
}

for _, testCase := range tests {
Expand Down
4 changes: 4 additions & 0 deletions internal/audiobooks/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ func (s *Service) GetAudiobooksByGenre(ctx context.Context, genre audiobooks.Gen
return s.audiobookStore.Get(ctx, GenreFilter(genre))
}

func (s *Service) GetAudiobooksByNarrator(ctx context.Context, name string) ([]audiobooks.Audiobook, error) {
return s.audiobookStore.Get(ctx, NarratorFilter(name))
}

func (s *Service) GetAudiobooksBy(ctx context.Context, filter func(*audiobooks.Audiobook) bool) ([]audiobooks.Audiobook, error) {
return s.audiobookStore.Get(ctx, filter)
}
46 changes: 44 additions & 2 deletions internal/podcasts/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ import (
const (
allAudiobooksFeedTitle = "Audiobooks"
allAudiobooksFeedDescription = "Like movies in your mind!"
genreFeedDescriptionFormat = "%s Audiobooks"
descriptionFormat = "%s Audiobooks"
authorDescriptionFormat = "Audiobooks by %s"
narratorDescriptionFormat = "Audiobooks Narrated by %s"
)

type AudiobooksClient interface {
GetAllAudiobooks(ctx context.Context) ([]audiobooks.Audiobook, error)
GetAudiobooksByGenre(ctx context.Context, genre audiobooks.Genre) ([]audiobooks.Audiobook, error)
GetAudiobooksByAuthor(ctx context.Context, author string) (books []audiobooks.Audiobook, err error)
GetAudiobooksByNarrator(ctx context.Context, narrator string) (books []audiobooks.Audiobook, err error)
UpdateAudiobooks(ctx context.Context) error
}

Expand Down Expand Up @@ -62,7 +66,7 @@ func (s *Service) WriteGenreAudiobookFeed(ctx context.Context, genre audiobooks.
}
genreFeedOpts := &FeedOpts{
Title: genre.String(),
Description: fmt.Sprintf(genreFeedDescriptionFormat, genre),
Description: fmt.Sprintf(descriptionFormat, genre),
Link: s.host,
ImageLink: s.fedImageLink,
Explicit: s.feedExplicit,
Expand All @@ -74,6 +78,28 @@ func (s *Service) WriteGenreAudiobookFeed(ctx context.Context, genre audiobooks.
return s.WriteFeedFromAudiobooks(ctx, books, genreFeedOpts, writer)
}

func (s *Service) WriteAuthorAudiobookFeed(ctx context.Context, author string, writer io.Writer) (bool, error) {
books, err := s.GetAudiobooksByAuthor(ctx, author)
if err != nil {
return false, err
}
if len(books) < 1 {
return false, nil
}
return true, s.writePersonAudiobookFeed(ctx, author, authorDescriptionFormat, books, writer)
}

func (s *Service) WriteNarratorAudiobookFeed(ctx context.Context, narrator string, writer io.Writer) (bool, error) {
books, err := s.GetAudiobooksByNarrator(ctx, narrator)
if err != nil {
return false, err
}
if len(books) < 1 {
return false, nil
}
return true, s.writePersonAudiobookFeed(ctx, narrator, narratorDescriptionFormat, books, writer)
}

func (s *Service) IsReady(ctx context.Context) bool {
return true
}
Expand All @@ -92,3 +118,19 @@ func New(audiobooksClient AudiobooksClient, logger loggerrific.Logger, opts ...O
}
return svc
}

func (s *Service) writePersonAudiobookFeed(ctx context.Context, personName, descFormat string,
personBooks []audiobooks.Audiobook, writer io.Writer) error {
personFeedOpts := &FeedOpts{
Title: personName,
Description: fmt.Sprintf(descFormat, personName),
Link: s.host,
ImageLink: s.fedImageLink,
Explicit: s.feedExplicit,
Language: s.feedLanguage,
Author: s.feedAuthor,
Email: s.feedAuthorEmail,
Copyright: s.feedCopyright,
}
return s.WriteFeedFromAudiobooks(ctx, personBooks, personFeedOpts, writer)
}
38 changes: 30 additions & 8 deletions internal/podcasts/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,44 @@ func (c *testAudiobookClient) GetAudiobooksByGenre(ctx context.Context, genre au
return testbooks.AudiobooksFilteredBy(testbooks.GenreFilter(genre)), nil
}

func (c *testAudiobookClient) GetAudiobooksByAuthor(ctx context.Context, author string) ([]audiobooks.Audiobook, error) {
return testbooks.AudiobooksFilteredBy(testbooks.AuthorFilter(author)), nil
}

func (c *testAudiobookClient) GetAudiobooksByNarrator(ctx context.Context, narrator string) ([]audiobooks.Audiobook, error) {
return testbooks.AudiobooksFilteredBy(testbooks.NarratorFilter(narrator)), nil
}

func (c *testAudiobookClient) UpdateAudiobooks(ctx context.Context) error {
return nil
}

func TestGetFeed(t *testing.T) {
tests := []struct {
name string
writeFeedTest func(*Service, io.Writer) error
writeFeedTest func(*Service, io.Writer) (bool, error)
pathToExpectedFeed string
expectedFeed string
expectedFeedExists bool
}{
{name: "Full feed", writeFeedTest: func(svc *Service, wrt io.Writer) error {
return svc.WriteAllAudiobooksFeed(context.Background(), wrt)
}, pathToExpectedFeed: "full_feed.rss"},
{name: "Sci-Fi feed", writeFeedTest: func(svc *Service, wrt io.Writer) error {
return svc.WriteGenreAudiobookFeed(context.Background(), audiobooks.SciFi, wrt)
}, pathToExpectedFeed: "scifi_feed.rss"},
{name: "Full feed", writeFeedTest: func(svc *Service, wrt io.Writer) (bool, error) {
return true, svc.WriteAllAudiobooksFeed(context.Background(), wrt)
}, pathToExpectedFeed: "full_feed.rss", expectedFeedExists: true},
{name: "Sci-Fi feed", writeFeedTest: func(svc *Service, wrt io.Writer) (bool, error) {
return true, svc.WriteGenreAudiobookFeed(context.Background(), audiobooks.SciFi, wrt)
}, pathToExpectedFeed: "scifi_feed.rss", expectedFeedExists: true},
{name: "Amal El-Mohtar Author feed", writeFeedTest: func(svc *Service, wrt io.Writer) (bool, error) {
return svc.WriteAuthorAudiobookFeed(context.Background(), "Amal El-Mohtar", wrt)
}, pathToExpectedFeed: "el_mohtar_feed.rss", expectedFeedExists: true},
{name: "Feed for author that does not exist in library", writeFeedTest: func(svc *Service, wrt io.Writer) (bool, error) {
return svc.WriteAuthorAudiobookFeed(context.Background(), "Octavia Butler", wrt)
}, expectedFeed: "", expectedFeedExists: false},
{name: "Kobna Holdbrook-Smith Narrator feed", writeFeedTest: func(svc *Service, wrt io.Writer) (bool, error) {
return svc.WriteNarratorAudiobookFeed(context.Background(), "Kobna Holdbrook-Smith", wrt)
}, pathToExpectedFeed: "holdbrook_smith_feed.rss", expectedFeedExists: true},
{name: "Feed for narrator that does not exist in library", writeFeedTest: func(svc *Service, wrt io.Writer) (bool, error) {
return svc.WriteNarratorAudiobookFeed(context.Background(), "Simon Vance", wrt)
}, expectedFeed: "", expectedFeedExists: false},
}

svc := New(&testAudiobookClient{},
Expand All @@ -66,10 +87,11 @@ func TestGetFeed(t *testing.T) {
var buf bytes.Buffer

// when
err := testCase.writeFeedTest(svc, &buf)
feedExists, err := testCase.writeFeedTest(svc, &buf)

// then
assert.NoError(t, err)
assert.Equal(t, testCase.expectedFeedExists, feedExists)
assert.Equal(t, expected, buf.String())
})
}
Expand Down
28 changes: 28 additions & 0 deletions internal/podcasts/service/testdata/el_mohtar_feed.rss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>Amal El-Mohtar</title>
<link>http://www.example-podcast.com/audiobooks</link>
<copyright></copyright>
<language>EN</language>
<description>Audiobooks by Amal El-Mohtar</description>
<itunes:author>A Person</itunes:author>
<itunes:block>yes</itunes:block>
<itunes:explicit>yes</itunes:explicit>
<itunes:owner>
<itunes:name>A Person</itunes:name>
<itunes:email>person@domain.test</itunes:email>
</itunes:owner>
<itunes:image href="http://www.example-podcast.com/images/itunes.jpg"></itunes:image>
<item>
<title>This Is How You Lose the Time War</title>
<guid>http://www.example-podcast.com/audiobooks/media/Amal%20El-Mohtar%20and%20Max%20Gladstone/This%20Is%20How%20You%20Lose%20the%20Time%20War/This%20Is%20How%20You%20Lose%20the%20Time%20War.m4b</guid>
<pubDate>Tue, 16 Jul 2019 08:00:00 +0000</pubDate>
<description><![CDATA[This Is How You Lose the Time War by Amal El-Mohtar & Max Gladstone]]></description>
<content:encoded><![CDATA[<h1>This Is How You Lose the Time War</h1><h2>By Amal El-Mohtar & Max Gladstone</h2><h4>Narrated by Cynthia Farrell & Emily Woo Zeller</h4><p>Among the ashes of a dying world, an agent of the Commandant finds a letter. It reads: Burn before reading.</p><p>Thus begins an unlikely correspondence between two rival agents hellbent on securing the best possible future for their warring factions. Now, what began as a taunt, a battlefield boast, grows into something more. Something epic. Something romantic. Something that could change the past and the future.</p><p>Except the discovery of their bond would mean death for each of them. There's still a war going on, after all. And someone has to win that war. That's how war works. Right?</p>]]></content:encoded>
<itunes:duration>0:04</itunes:duration>
<itunes:subtitle>Amal El-Mohtar &amp; Max Gladstone</itunes:subtitle>
<enclosure url="http://www.example-podcast.com/audiobooks/media/Amal%20El-Mohtar%20and%20Max%20Gladstone/This%20Is%20How%20You%20Lose%20the%20Time%20War/This%20Is%20How%20You%20Lose%20the%20Time%20War.m4b" length="145608" type="audio/mp4a-latm"></enclosure>
</item>
</channel>
</rss>
28 changes: 28 additions & 0 deletions internal/podcasts/service/testdata/holdbrook_smith_feed.rss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>Kobna Holdbrook-Smith</title>
<link>http://www.example-podcast.com/audiobooks</link>
<copyright></copyright>
<language>EN</language>
<description>Audiobooks Narrated by Kobna Holdbrook-Smith</description>
<itunes:author>A Person</itunes:author>
<itunes:block>yes</itunes:block>
<itunes:explicit>yes</itunes:explicit>
<itunes:owner>
<itunes:name>A Person</itunes:name>
<itunes:email>person@domain.test</itunes:email>
</itunes:owner>
<itunes:image href="http://www.example-podcast.com/images/itunes.jpg"></itunes:image>
<item>
<title>A Wizard of Earthsea</title>
<guid>http://www.example-podcast.com/audiobooks/media/Ursula%20K%20Le%20Guin/Earthsea/1%20A%20Wizard%20of%20Earthsea/A%20Wizard%20of%20Earthsea.m4b</guid>
<pubDate>Thu, 01 Jan 1970 08:00:00 +0000</pubDate>
<description><![CDATA[A Wizard of Earthsea by Ursula K. Le Guin]]></description>
<content:encoded><![CDATA[<h1>A Wizard of Earthsea</h1><h2>By Ursula K. Le Guin</h2><h4>Earthsea Book 1</h4><h4>Narrated by Kobna Holdbrook-Smith</h4><p>Ged, the greatest sorcerer in all Earthsea, was called Sparrowhawk in his reckless youth.</p><p>Hungry for power and knowledge, Sparrowhawk tampered with long-held secrets and loosed a terrible shadow upon the world. This is the tale of his testing, how he mastered the mighty words of power, tamed an ancient dragon, and crossed death's threshold to restore the balance.</p>]]></content:encoded>
<itunes:duration>0:04</itunes:duration>
<itunes:subtitle>Ursula K. Le Guin</itunes:subtitle>
<enclosure url="http://www.example-podcast.com/audiobooks/media/Ursula%20K%20Le%20Guin/Earthsea/1%20A%20Wizard%20of%20Earthsea/A%20Wizard%20of%20Earthsea.m4b" length="145565" type="audio/mp4a-latm"></enclosure>
</item>
</channel>
</rss>
9 changes: 9 additions & 0 deletions internal/testing/testbooks/testbooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ func AuthorFilter(name string) Filter {
}
}

func NarratorFilter(name string) Filter {
return func(a *audiobooks.Audiobook) bool {
if a != nil && contains(a.Narrators, name) {
return true
}
return false
}
}

func GenreFilter(genre audiobooks.Genre) Filter {
return func(a *audiobooks.Audiobook) bool {
if a != nil && contains(a.Genres, genre) {
Expand Down
8 changes: 5 additions & 3 deletions internal/transport/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import (
)

type AudiobooksPodcastService interface {
WriteAllAudiobooksFeed(ctx context.Context, w io.Writer) error
WriteAllAudiobooksFeed(context.Context, io.Writer) error
WriteGenreAudiobookFeed(context.Context, audiobooks.Genre, io.Writer) error
WriteAuthorAudiobookFeed(context.Context, string, io.Writer) (bool, error)
WriteNarratorAudiobookFeed(context.Context, string, io.Writer) (bool, error)
UpdateFeeds(context.Context) error
IsReady(ctx context.Context) bool
}
Expand Down Expand Up @@ -69,14 +71,14 @@ func (h *Handler) mapRoutes() {
middleware := NewMiddlewares(h.Log, h.CacheStore)

podcastSubrouter := h.PathPrefix(h.podcastServePath).Subrouter()
h.Log.Infoln("Cache store is", middleware.CacheStore)
h.Log.Infoln("Cache store exists is ", (middleware.CacheStore != nil))
if middleware.CacheStore != nil {
h.Log.Infoln("Caching enabled on", h.podcastServePath, "endpoints is enabled with at TTL of", middleware.CacheStore.GetTTL().String())
}
podcastSubrouter.Use(middleware.LoggingMiddleware, middleware.CachingMiddleware)
podcastSubrouter.HandleFunc(h.mainFeedPath, h.getFeed)
podcastSubrouter.HandleFunc(fmt.Sprintf("/genre/{genre}%s", h.mainFeedPath), h.getGenreFeed)
podcastSubrouter.HandleFunc(fmt.Sprintf("/authors/{author}%s", h.mainFeedPath), h.getAuthorFeed)
podcastSubrouter.HandleFunc(fmt.Sprintf("/narrators/{narrator}%s", h.mainFeedPath), h.getNarratorFeed)

updateRouter := h.PathPrefix("/update").Subrouter()
updateRouter.Use(SevereRateLimitMiddleware, middleware.LoggingMiddleware)
Expand Down
Loading

0 comments on commit 68984ba

Please sign in to comment.