Skip to content

Commit

Permalink
feat(kemono): Add podcast endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe565 committed Jun 26, 2024
1 parent 35d3f29 commit fb5e100
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ An Atom feed is generated by default, but a file extension of
### Kemono

- `/kemono/{service}/user/{name}`
- `/kemono/{service}/podcast/{name}`

#### Query Parameters
| Name | Description | Default |
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.4
require (
github.com/Masterminds/sprig/v3 v3.2.3
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0
github.com/eduncan911/podcast v1.4.2
github.com/go-chi/chi/v5 v5.0.12
github.com/google/go-containerregistry v0.19.1
github.com/google/go-github/v62 v62.0.0
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4B
github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down Expand Up @@ -146,6 +148,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
63 changes: 37 additions & 26 deletions internal/feed/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path"
"strings"

"github.com/eduncan911/podcast"
"github.com/go-chi/chi/v5"
"github.com/gorilla/feeds"
)
Expand Down Expand Up @@ -56,39 +57,49 @@ func SetType(next http.Handler) http.Handler {

func WriteFeed(w http.ResponseWriter, r *http.Request) error {
format := r.Context().Value(TypeKey).(OutputFormat)
feed := r.Context().Value(FeedKey).(*feeds.Feed)
feed := r.Context().Value(FeedKey)

var buf bytes.Buffer

switch format {
case OutputAtom, OutputUnknown:
atomFeed := (&feeds.Atom{Feed: feed}).AtomFeed()
if feed.Image != nil {
atomFeed.Icon = feed.Image.Url
}
if err := feeds.WriteXML(atomFeed, &buf); err != nil {
return err
}
w.Header().Set("Content-Type", "application/rss+xml")
case OutputJSON:
jsonFeed := (&feeds.JSON{Feed: feed}).JSONFeed()
if feed.Image != nil {
jsonFeed.Icon = feed.Image.Url
}
e := json.NewEncoder(&buf)
e.SetIndent("", " ")
if err := e.Encode(jsonFeed); err != nil {
return err
switch feed := feed.(type) {
case *feeds.Feed:
switch format {
case OutputAtom, OutputUnknown:
atomFeed := (&feeds.Atom{Feed: feed}).AtomFeed()
if feed.Image != nil {
atomFeed.Icon = feed.Image.Url
}
if err := feeds.WriteXML(atomFeed, &buf); err != nil {
return err
}
w.Header().Set("Content-Type", "application/rss+xml")
case OutputJSON:
jsonFeed := (&feeds.JSON{Feed: feed}).JSONFeed()
if feed.Image != nil {
jsonFeed.Icon = feed.Image.Url
}
e := json.NewEncoder(&buf)
e.SetIndent("", " ")
if err := e.Encode(jsonFeed); err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
case OutputRSS:
if err := feed.WriteRss(&buf); err != nil {
return err
}
w.Header().Set("Content-Type", "application/rss+xml")
default:
http.Error(w, "400 invalid format", http.StatusBadRequest)
return nil
}
w.Header().Set("Content-Type", "application/json")
case OutputRSS:
if err := feed.WriteRss(&buf); err != nil {
case *podcast.Podcast:
if err := feed.Encode(&buf); err != nil {
return err
}
w.Header().Set("Content-Type", "application/rss+xml")
w.Header().Set("Content-Type", "application/xml")
default:
http.Error(w, "400 invalid format", http.StatusBadRequest)
return nil
panic("invalid feed type")
}

if _, err := io.Copy(w, &buf); err != nil {
Expand Down
53 changes: 53 additions & 0 deletions internal/kemono/creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strconv"
"time"

"github.com/eduncan911/podcast"
"github.com/gabe565/transsmute/internal/util"
"github.com/gorilla/feeds"
"github.com/jellydator/ttlcache/v3"
Expand Down Expand Up @@ -212,3 +213,55 @@ func (c *Creator) Feed(ctx context.Context, pages uint64, tag, query string) (*f

return f, nil
}

func (c *Creator) Podcast(ctx context.Context, pages uint64, tag string) (*podcast.Podcast, error) {
var pubdate *time.Time
if c.Indexed != 0 {
d := time.Unix(int64(c.Updated), 0)
pubdate = &d
}
f := podcast.New(c.Name, c.PublicURL().String(), "", pubdate, nil)
f.IBlock = "Yes"

for page := range pages {
posts, err := c.FetchPostPage(ctx, page, "")
if err != nil {
return nil, err
}
f.Items = slices.Grow(f.Items, len(posts))

for _, post := range posts {
if tag != "" {
tags, err := post.Tags()
if err != nil {
return nil, err
}
if !slices.Contains(tags, tag) {
continue
}
}

item, image, err := post.PodcastItem(ctx)
if err != nil {
if errors.Is(err, ErrNoAudio) {
continue
}
return nil, err
}
f.Items = append(f.Items, item)
if image != nil && f.Image == nil {
f.AddImage(image.ThumbURL().String())
}
}

if len(posts) < 50 {
break
}
}

if f.Image == nil {
f.AddImage(c.ImageURL().String())
}

return &f, nil
}
55 changes: 55 additions & 0 deletions internal/kemono/podcast_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package kemono

import (
"context"
"errors"
"net/http"
"strconv"

"github.com/gabe565/transsmute/internal/feed"
"github.com/gabe565/transsmute/internal/util"
"github.com/go-chi/chi/v5"
)

func podcastHandler(host string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
creator, err := GetCreatorInfo(r.Context(), host, chi.URLParam(r, "service"), chi.URLParam(r, "creator"))
if err != nil {
if errors.Is(err, ErrCreatorNotFound) {
http.Error(w, err.Error(), http.StatusNotFound)
return
} else if errors.Is(err, util.ErrUpstreamRequest) {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
panic(err)
}

pagesRaw := r.URL.Query().Get("pages")
pages := uint64(1)
if pagesRaw != "" {
var err error
if pages, err = strconv.ParseUint(pagesRaw, 10, 64); err != nil || pages == 0 {
http.Error(w, "pages must be a positive integer", http.StatusBadRequest)
return
}
}

tag := r.URL.Query().Get("tag")

f, err := creator.Podcast(r.Context(), pages, tag)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
panic(err)
}

if val := r.URL.Query().Get("title"); val != "" {
f.Title = val
}

r = r.WithContext(context.WithValue(r.Context(), feed.FeedKey, f))
if err := feed.WriteFeed(w, r); err != nil {
panic(err)
}
}
}
49 changes: 49 additions & 0 deletions internal/kemono/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"context"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"

"github.com/eduncan911/podcast"
"github.com/gabe565/transsmute/internal/util"
"github.com/gorilla/feeds"
)
Expand Down Expand Up @@ -63,6 +66,47 @@ func (p *Post) FeedItem() *feeds.Item {
return item
}

var ErrNoAudio = errors.New("no audio file")

func (p *Post) PodcastItem(ctx context.Context) (*podcast.Item, *Attachment, error) {
var audio, image *Attachment
for _, attachment := range p.Attachments {
switch {
case attachment.IsAudio() && audio == nil:
audio = attachment
case attachment.IsImage() && image == nil:
image = attachment
}
}
if audio == nil {
return nil, nil, ErrNoAudio
}

audioInfo, err := audio.Info(ctx)
if err != nil {
return nil, nil, err
}

item := &podcast.Item{
Title: p.Title,
Link: p.creator.PostURL(p).String(),
Description: p.Content,
GUID: p.ID,
Enclosure: &podcast.Enclosure{
URL: audio.URL().String(),
LengthFormatted: strconv.Itoa(audioInfo.Size),
TypeFormatted: audioInfo.MIMEType,
},
}
if image != nil {
item.AddImage(image.ThumbURL().String())
}
if parsed, err := time.Parse("2006-01-02T15:04:05", p.Published); err == nil {
item.AddPubDate(&parsed)
}
return item, image, nil
}

type Embed struct {
URL string `json:"url"`
Subject string `json:"subject"`
Expand All @@ -85,6 +129,11 @@ func (a *Attachment) IsVideo() bool {
return ext == ".mp4" || ext == ".webm"
}

func (a *Attachment) IsAudio() bool {
ext := path.Ext(a.Path)
return ext == ".mp3" || ext == ".m4a"
}

func (a *Attachment) ThumbURL() *url.URL {
u := &url.URL{
Scheme: "https",
Expand Down
1 change: 1 addition & 0 deletions internal/kemono/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func Routes(r chi.Router, conf config.Kemono) error {

for name, host := range conf.Hosts {
r.Get("/"+name+"/{service}/user/{creator}", postHandler(host))
r.Get("/"+name+"/{service}/podcast/{creator}", podcastHandler(host))
}
}
return nil
Expand Down

0 comments on commit fb5e100

Please sign in to comment.