- commit
- ecd7c1577bd374c20eceb93c049a2e2af3627a2e
- parent
- 50d81de619689b199992d949a24518e49a3d4c71
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2025-12-21 11:50
refactor: use templates
Diffstat
| M | audiothek.go | 145 | +++++++++++++++++++++++++------------------------------------ |
| A | templates/show.m3u | 8 | ++++++++ |
| A | templates/show.rss | 37 | +++++++++++++++++++++++++++++++++++++ |
3 files changed, 104 insertions, 86 deletions
diff --git a/audiothek.go b/audiothek.go
@@ -3,10 +3,12 @@ package main 3 3 import ( 4 4 "bytes" 5 5 "context" -1 6 "embed" 6 7 "encoding/json" 7 8 "flag" 8 9 "fmt" 9 10 "io/ioutil" -1 11 "iter" 10 12 "log" 11 13 "net" 12 14 "net/http" @@ -73,6 +75,17 @@ var CATEGORIES = map[string]string{ 73 75 "Kultur": "Society & Culture", 74 76 } 75 77 -1 78 //go:embed templates -1 79 var tplData embed.FS -1 80 var tplFuncs = template.FuncMap{ -1 81 "reverse": reverse, -1 82 "trim": strings.TrimSpace, -1 83 "rssDate": rssDate, -1 84 "imgWidth": imgWidth, -1 85 "itunesCategory": itunesCategory, -1 86 } -1 87 var tpl = template.Must(template.New("root").Funcs(tplFuncs).ParseFS(tplData, "templates/*")) -1 88 76 89 type Item struct { 77 90 Title string `json:"title"` 78 91 SharingUrl string `json:"sharingUrl"` @@ -116,11 +129,25 @@ type Response struct { 116 129 } `json:"data"` 117 130 } 118 131119 -1 func escape(s string) string {120 -1 return template.HTMLEscapeString(s)-1 132 type ShowContext struct { -1 133 Request *http.Request -1 134 Show Show -1 135 Id string -1 136 Origin string -1 137 NextOffset int -1 138 } -1 139 -1 140 func reverse(items []Item) iter.Seq[Item] { -1 141 return func(yield func(Item) bool) { -1 142 for i := len(items) - 1; i >= 0; i-- { -1 143 if !yield(items[i]) { -1 144 return -1 145 } -1 146 } -1 147 } 121 148 } 122 149123 -1 func convertDate(s string) (string, error) {-1 150 func rssDate(s string) (string, error) { 124 151 t, err := time.Parse(time.RFC3339, s) 125 152 if err != nil { 126 153 return "", err @@ -128,6 +155,19 @@ func convertDate(s string) (string, error) { 128 155 return t.Format(time.RFC822), nil 129 156 } 130 157 -1 158 func imgWidth(width int, url string) string { -1 159 return strings.Replace(url, "{width}", strconv.Itoa(width), 1) -1 160 } -1 161 -1 162 func itunesCategory(category string) string { -1 163 itunesCategory, exists := CATEGORIES[category] -1 164 if exists { -1 165 return itunesCategory -1 166 } else { -1 167 return category -1 168 } -1 169 } -1 170 131 171 func fetchShow(id string, offset int) (Show, error) { 132 172 url := "https://api.ardaudiothek.de/graphql" 133 173 jsonValue, _ := json.Marshal(map[string]interface{}{ @@ -162,78 +202,6 @@ func fetchShow(id string, offset int) (Show, error) { 162 202 return *response.Data.Result, nil 163 203 } 164 204165 -1 func renderItem(w http.ResponseWriter, item Item) {166 -1 fmt.Fprintf(w, "\t\t<item>\n")167 -1 defer fmt.Fprintf(w, "\t\t</item>\n")168 -1 fmt.Fprintf(w, "\t\t\t<title>%s</title>\n", escape(item.Title))169 -1 fmt.Fprintf(w, "\t\t\t<link>%s</link>\n", escape(item.SharingUrl))170 -1 fmt.Fprintf(w, "\t\t\t<guid>%s</guid>\n", escape(item.SharingUrl))171 -1 publishDate, err := convertDate(item.PublishDate)172 -1 if err == nil {173 -1 fmt.Fprintf(w, "\t\t\t<pubDate>%s</pubDate>\n", escape(publishDate))174 -1 }175 -1 fmt.Fprintf(w, "\t\t\t<description>%s</description>\n", escape(item.Summary))176 -1 fmt.Fprintf(w, "\t\t\t<itunes:duration>%d</itunes:duration>\n", item.Duration)177 -1 fmt.Fprintf(w, "\t\t\t<itunes:image href=\"%s\" />\n", escape(178 -1 strings.Replace(item.Image.Url1X1, "{width}", "1400", 1),179 -1 ))180 -1 if len(item.Audios) > 0 {181 -1 audio := item.Audios[0]182 -1 fmt.Fprintf(w, "\t\t\t<enclosure url=\"%s\" length=\"%d\" type=\"%s\" />\n", escape(audio.Url), audio.Size, escape(audio.MimeType))183 -1 }184 -1 }185 -1186 -1 func renderFeed(w http.ResponseWriter, show Show, links map[string]string) {187 -1 w.Header().Set("Content-Type", "text/xml")188 -1 fmt.Fprintf(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")189 -1 fmt.Fprintf(w, "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:itunes=\"http://www.itunes.com/dtds/podcast-1.0.dtd\">\n")190 -1 defer fmt.Fprintf(w, "</rss>\n")191 -1 fmt.Fprintf(w, "\t<channel>\n")192 -1 defer fmt.Fprintf(w, "\t</channel>\n")193 -1 fmt.Fprintf(w, "\t\t<title>%s</title>\n", escape(show.Title))194 -1 fmt.Fprintf(w, "\t\t<description>%s</description>\n", escape(show.Synopsis))195 -1 fmt.Fprintf(w, "\t\t<link>%s</link>\n", escape(show.SharingUrl))196 -1 fmt.Fprintf(w, "\t\t<language>de-DE</language>\n")197 -1 fmt.Fprintf(w, "\t\t<itunes:author>%s</itunes:author>\n", escape(show.PublicationService.Title))198 -1 fmt.Fprintf(w, "\t\t<itunes:image href=\"%s\" />\n", escape(199 -1 strings.Replace(show.Image.Url1X1, "{width}", "1400", 1),200 -1 ))201 -1 fmt.Fprintf(w, "\t\t<generator>audiothek.ce9e.org</generator>\n")202 -1203 -1 if show.EditorialCategory.Title != "" {204 -1 category := strings.TrimSpace(show.EditorialCategory.Title)205 -1 fmt.Fprintf(w, "\t\t<category>%s</category>\n", escape(category))206 -1207 -1 itunesCategory, exists := CATEGORIES[category]208 -1 if !exists {209 -1 itunesCategory = category210 -1 }211 -1 fmt.Fprintf(w, "\t\t<itunes:category text=\"%s\" />\n", escape(itunesCategory))212 -1 }213 -1214 -1 n := len(show.Items.Nodes)215 -1 for i := range n {216 -1 renderItem(w, show.Items.Nodes[n-1-i])217 -1 }218 -1219 -1 for rel, href := range links {220 -1 fmt.Fprintf(w, "\t\t<atom:link href=\"%s\" rel=\"%s\" type=\"application/rss+xml\" />\n", escape(href), escape(rel))221 -1 }222 -1 }223 -1224 -1 func renderPlaylist(w http.ResponseWriter, show Show) {225 -1 w.Header().Set("Content-Type", "text/plain")226 -1 fmt.Fprintf(w, "#EXTM3U\n")227 -1 n := len(show.Items.Nodes)228 -1 for i := range n {229 -1 item := show.Items.Nodes[n-1-i]230 -1 if len(item.Audios) > 0 {231 -1 fmt.Fprintf(w, "#EXTINF:%d,%s\n", item.Duration, item.Title)232 -1 fmt.Fprintf(w, "%s\n", item.Audios[0].Url)233 -1 }234 -1 }235 -1 }236 -1237 205 func handle(w http.ResponseWriter, r *http.Request) { 238 206 matches := urlPattern.FindStringSubmatch(r.URL.Path) 239 207 @@ -254,19 +222,24 @@ func handle(w http.ResponseWriter, r *http.Request) { 254 222 return 255 223 } 256 224257 -1 if matches[2] == "rss" {258 -1 links := make(map[string]string)259 -1260 -1 if origin != "" {261 -1 links["self"] = fmt.Sprintf("%s%s", origin, r.URL.RequestURI())262 -1 if show.Items.PageInfo.HasNextPage {263 -1 links["next"] = fmt.Sprintf("%s%s?offset=%d", origin, r.URL.Path, offset+COUNT)264 -1 }265 -1 }-1 225 ctx := ShowContext{ -1 226 Request: r, -1 227 Show: show, -1 228 Id: matches[1], -1 229 Origin: origin, -1 230 NextOffset: offset + COUNT, -1 231 } 266 232267 -1 renderFeed(w, show, links)-1 233 if matches[2] == "rss" { -1 234 w.Header().Set("Content-Type", "text/xml") -1 235 err = tpl.ExecuteTemplate(w, "show.rss", ctx) 268 236 } else if matches[2] == "m3u" {269 -1 renderPlaylist(w, show)-1 237 w.Header().Set("Content-Type", "text/plain") -1 238 err = tpl.ExecuteTemplate(w, "show.m3u", ctx) -1 239 } -1 240 -1 241 if err != nil { -1 242 log.Printf("ERROR: %s", err) 270 243 } 271 244 } 272 245
diff --git a/templates/show.m3u b/templates/show.m3u
@@ -0,0 +1,8 @@
-1 1 #EXTM3U
-1 2 {{- range reverse .Show.Items.Nodes}}
-1 3 {{- if gt (len .Audios) 0 }}
-1 4 #EXTINF:{{.Duration}},{{.Title}}
-1 5 {{- $audio := index .Audios 0 }}
-1 6 {{ $audio.Url }}
-1 7 {{- end}}
-1 8 {{- end}}
diff --git a/templates/show.rss b/templates/show.rss
@@ -0,0 +1,37 @@
-1 1 <?xml version="1.0" encoding="UTF-8"?>
-1 2 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
-1 3 <channel>
-1 4 <title>{{.Show.Title | html}}</title>
-1 5 <description>{{.Show.Synopsis | html}}</description>
-1 6 <link>{{.Show.SharingUrl | html}}</link>
-1 7 <language>de-DE</language>
-1 8 <itunes:author>{{.Show.PublicationService.Title | html}}</itunes:author>
-1 9 <itunes:image href="{{.Show.Image.Url1X1 | imgWidth 1400 | html}}" />
-1 10 <generator>audiothek.ce9e.org</generator>
-1 11 {{- if ne .Show.EditorialCategory.Title ""}}
-1 12 <category>{{.Show.EditorialCategory.Title | trim | html}}</category>
-1 13 <itunes:category text="{{.Show.EditorialCategory.Title | trim | itunesCategory | html}}" />
-1 14 {{- end}}
-1 15 {{- range reverse .Show.Items.Nodes }}
-1 16 <item>
-1 17 <title>{{.Title | html}}</title>
-1 18 <link>{{.SharingUrl | html}}</link>
-1 19 <guid>{{.SharingUrl | html}}</guid>
-1 20 <pubDate>{{.PublishDate | rssDate | html}}</pubDate>
-1 21 <description>{{.Summary | html}}</description>
-1 22 <itunes:duration>{{.Duration | html}}</itunes:duration>
-1 23 <itunes:image href="{{.Image.Url1X1 | imgWidth 1400 | html}}" />
-1 24 {{- if gt (len .Audios) 0 }}
-1 25 {{- $audio := index .Audios 0 }}
-1 26 <enclosure url="{{$audio.Url | html}}" length="{{$audio.Size | html}}" type="{{$audio.MimeType | html}}" />
-1 27 {{- end}}
-1 28 </item>
-1 29 {{- end}}
-1 30 {{- if ne .Origin ""}}
-1 31 <atom:link href="{{.Origin | html}}{{.Request.URL.RequestURI | html}}" rel="self" type="application/rss+xml" />
-1 32 {{- if .Show.Items.PageInfo.HasNextPage}}
-1 33 <atom:link href="{{.Origin | html}}{{.Request.URL.Path | html}}?offset={{.NextOffset | html }}" rel="next" type="application/rss+xml" />
-1 34 {{- end}}
-1 35 {{- end}}
-1 36 </channel>
-1 37 </rss>