audiothek

alternative frontend for ardaudiothek.de  https://audiothek.ce9e.org/
git clone https://git.ce9e.org/audiothek.git

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   131 
  119    -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   149 
  123    -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   204 
  165    -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    -1 
  186    -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    -1 
  203    -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    -1 
  207    -1 		itunesCategory, exists := CATEGORIES[category]
  208    -1 		if !exists {
  209    -1 			itunesCategory = category
  210    -1 		}
  211    -1 		fmt.Fprintf(w, "\t\t<itunes:category text=\"%s\" />\n", escape(itunesCategory))
  212    -1 	}
  213    -1 
  214    -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    -1 
  219    -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    -1 
  224    -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    -1 
  237   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   224 
  257    -1 	if matches[2] == "rss" {
  258    -1 		links := make(map[string]string)
  259    -1 
  260    -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   232 
  267    -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>