audiothek

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

commit
c8d7565e0e3b0b4ae71ae440563552340363d482
parent
881e58f9232a7892d4a16fab42a2e2dd03017dec
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2025-12-21 23:31
add search view

Diffstat

M audiothek.go 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
M index.html 7 +++++++
M style.css 27 +++++++++++++++++++++++++++
C templates/show.html -> templates/search.html 38 ++++++++++++--------------------------
M templates/show.html 7 +++++++

5 files changed, 133 insertions, 28 deletions


diff --git a/audiothek.go b/audiothek.go

@@ -53,6 +53,24 @@ query ProgramSetEpisodesQuery($id:ID!, $offset:Int!, $count:Int!) {
   53    53   }
   54    54 }`
   55    55 
   -1    56 const SEARCH_QUERY = `
   -1    57 query Search($q:String!) {
   -1    58   programSets(orderBy:LAST_ITEM_MODIFIED_DESC, filter:{numberOfElements:{greaterThan: 0}, or:[{title:{likeInsensitive:$q}}, {synopsis:{likeInsensitive:$q}}]}, first: 50) {
   -1    59     nodes{
   -1    60       id
   -1    61       coreId
   -1    62       title
   -1    63       synopsis
   -1    64       sharingUrl
   -1    65       numberOfElements
   -1    66       lastItemModified
   -1    67       image{ url1X1 }
   -1    68       publicationService{ id title }
   -1    69       editorialCategory{ id title }
   -1    70     }
   -1    71   }
   -1    72 }`
   -1    73 
   56    74 var CATEGORIES = map[string]string{
   57    75 	"Doku & Reportage":         "Society & Culture",
   58    76 	"True Crime":               "True Crime",
@@ -104,6 +122,7 @@ type Item struct {
  104   122 }
  105   123 
  106   124 type Show struct {
   -1   125 	CoreId     string `json:"coreId"`
  107   126 	Title      string `json:"title"`
  108   127 	Synopsis   string `json:"synopsis"`
  109   128 	SharingUrl string `json:"sharingUrl"`
@@ -124,12 +143,20 @@ type Show struct {
  124   143 	} `json:"items"`
  125   144 }
  126   145 
  127    -1 type Response struct {
   -1   146 type ShowResponse struct {
  128   147 	Data struct {
  129   148 		Result *Show `json:"result"`
  130   149 	} `json:"data"`
  131   150 }
  132   151 
   -1   152 type SearchResponse struct {
   -1   153 	Data struct {
   -1   154 		ProgramSets struct {
   -1   155 			Nodes []Show `json:"nodes"`
   -1   156 		} `json:"programSets"`
   -1   157 	} `json:"data"`
   -1   158 }
   -1   159 
  133   160 type ShowContext struct {
  134   161 	Request    *http.Request
  135   162 	Show       Show
@@ -138,6 +165,13 @@ type ShowContext struct {
  138   165 	NextOffset int
  139   166 }
  140   167 
   -1   168 type SearchContext struct {
   -1   169 	Request *http.Request
   -1   170 	Origin  string
   -1   171 	Q       string
   -1   172 	Shows   []Show
   -1   173 }
   -1   174 
  141   175 func reverse(items []Item) iter.Seq[Item] {
  142   176 	return func(yield func(Item) bool) {
  143   177 		for i := len(items) - 1; i >= 0; i-- {
@@ -199,7 +233,7 @@ func fetchShow(id string, offset int) (Show, error) {
  199   233 	}
  200   234 
  201   235 	body, _ := ioutil.ReadAll(resp.Body)
  202    -1 	var response Response
   -1   236 	var response ShowResponse
  203   237 	if err = json.Unmarshal(body, &response); err != nil {
  204   238 		return Show{}, err
  205   239 	}
@@ -255,6 +289,49 @@ func handle(w http.ResponseWriter, r *http.Request) {
  255   289 	}
  256   290 }
  257   291 
   -1   292 func handleSearch(w http.ResponseWriter, r *http.Request) {
   -1   293 	q := r.URL.Query().Get("q")
   -1   294 
   -1   295 	url := "https://api.ardaudiothek.de/graphql"
   -1   296 	jsonValue, _ := json.Marshal(map[string]interface{}{
   -1   297 		"query": SEARCH_QUERY,
   -1   298 		"variables": map[string]interface{}{
   -1   299 			"q": fmt.Sprintf("%%%s%%", strings.ReplaceAll(q, " ", "%")),
   -1   300 		},
   -1   301 	})
   -1   302 
   -1   303 	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonValue))
   -1   304 	if err != nil {
   -1   305 		http.Error(w, err.Error(), http.StatusNotFound)
   -1   306 		return
   -1   307 	}
   -1   308 	defer resp.Body.Close()
   -1   309 
   -1   310 	if resp.StatusCode != http.StatusOK {
   -1   311 		http.Error(w, "Internal Server Error", http.StatusNotFound)
   -1   312 		return
   -1   313 	}
   -1   314 
   -1   315 	body, _ := ioutil.ReadAll(resp.Body)
   -1   316 	var response SearchResponse
   -1   317 	if err = json.Unmarshal(body, &response); err != nil {
   -1   318 		http.Error(w, err.Error(), http.StatusNotFound)
   -1   319 		return
   -1   320 	}
   -1   321 
   -1   322 	ctx := SearchContext{
   -1   323 		Request: r,
   -1   324 		Origin:  origin,
   -1   325 		Q:       q,
   -1   326 		Shows:   response.Data.ProgramSets.Nodes,
   -1   327 	}
   -1   328 	w.Header().Set("Content-Type", "text/html")
   -1   329 	err = tpl.ExecuteTemplate(w, "search.html", ctx)
   -1   330 	if err != nil {
   -1   331 		log.Printf("ERROR: %s", err)
   -1   332 	}
   -1   333 }
   -1   334 
  258   335 func main() {
  259   336 	port := "8000"
  260   337 
@@ -270,6 +347,7 @@ func main() {
  270   347 	addr := fmt.Sprintf("localhost:%s", port)
  271   348 
  272   349 	http.HandleFunc("/", handle)
   -1   350 	http.HandleFunc("/search/", handleSearch)
  273   351 
  274   352 	ctx, unregisterSignals := signal.NotifyContext(
  275   353 		context.Background(), os.Interrupt, syscall.SIGTERM,

diff --git a/index.html b/index.html

@@ -7,6 +7,13 @@
    7     7 	<link rel="stylesheet" type="text/css" href="style.css">
    8     8 </head>
    9     9 <body>
   -1    10 	<header>
   -1    11 		<h1><a href="/" rel="home">Audiothek</a></h1>
   -1    12 		<form action="/search/" role="search">
   -1    13 			<input type="search" name="q">
   -1    14 			<button class="btn">Suche</button>
   -1    15 		</form>
   -1    16 	</header>
   10    17 	<h1>RSS Feeds für die ARD Audiothek</h1>
   11    18 	<pre><code>https://audiothek.ce9e.org/urn:ard:show:{id}.rss</code></pre>
   12    19 	<p>Die ID findet man in der jeweiligen URL in der <a href="https://www.ardaudiothek.de/">ARD Audiothek</a>.</p>

diff --git a/style.css b/style.css

@@ -34,6 +34,7 @@ blockquote {
   34    34 }
   35    35 
   36    36 pre,
   -1    37 input,
   37    38 .btn {
   38    39 	font-family: inherit;
   39    40 	font-size: inherit;
@@ -45,6 +46,7 @@ pre,
   45    46 }
   46    47 
   47    48 pre,
   -1    49 input,
   48    50 .btn:hover {
   49    51 	background-color: #243350;
   50    52 }
@@ -56,6 +58,31 @@ pre {
   56    58 	font-size: 125%;
   57    59 }
   58    60 
   -1    61 header {
   -1    62 	display: flex;
   -1    63 	justify-content: space-between;
   -1    64 	align-items: center;
   -1    65 	padding-block: 1em;
   -1    66 	margin-block-end: 3em;
   -1    67 	gap: 1em;
   -1    68 }
   -1    69 
   -1    70 h1 {
   -1    71 	margin: 0;
   -1    72 }
   -1    73 h1 a {
   -1    74 	color: inherit;
   -1    75 	text-decoration: none;
   -1    76 }
   -1    77 
   -1    78 [role="search"] {
   -1    79 	display: flex;
   -1    80 	gap: 0.5em;
   -1    81 }
   -1    82 [role="search"] input {
   -1    83 	flex: 1;
   -1    84 }
   -1    85 
   59    86 .center {
   60    87 	margin-block: 0 4em;
   61    88 	text-align: center;

diff --git a/templates/show.html b/templates/search.html

@@ -3,44 +3,30 @@
    3     3 <head>
    4     4 	<meta charset="utf-8">
    5     5 	<meta name="viewport" content="width=device-width">
    6    -1 	<title>{{.Show.Title | html}} &middot; Audiothek</title>
   -1     6 	<title>Suche &middot Audiothek</title>
    7     7 	<link rel="stylesheet" type="text/css" href="/style.css">
    8    -1 	<link rel="alternate" type="application/rss+xml" href="/{{.Id | html}}.rss">
    9     8 </head>
   10     9 <body>
   -1    10 	<header id="header-bar">
   -1    11 		<h1><a href="/" rel="home">Audiothek</a></h1>
   -1    12 		<form action="/search/" role="search">
   -1    13 			<input type="search" name="q" value="{{.Q | html}}">
   -1    14 			<button class="btn">Suche</button>
   -1    15 		</form>
   -1    16 	</header>
   11    17 	<main>
   12    -1 		<div class="center">
   13    -1 			<h2>{{.Show.Title | html}}</h2>
   14    -1 			<div>
   15    -1 				{{.Show.PublicationService.Title | html}}
   16    -1 				{{- if .Show.EditorialCategory.Title}}
   17    -1 				&middot; {{.Show.EditorialCategory.Title | html}}
   18    -1 				{{- end}}
   19    -1 				&middot; <a href="/{{.Id | html}}.rss" rel="alternate">RSS</a>
   20    -1 				&middot; <a href="/{{.Id | html}}.m3u" rel="alternate">M3U</a>
   21    -1 				&middot; <a href="{{.Show.SharingUrl | html}}" rel="alternate">Audiothek</a>
   22    -1 			</div>
   23    -1 			<p>{{.Show.Synopsis | html}}</p>
   24    -1 		</div>
   25    18 		<ul class="items">
   26    -1 			{{- range .Show.Items.Nodes}}
   27    -1 			{{- $audio := index .Audios 0 }}
   -1    19 			{{- range .Shows}}
   28    20 			<li>
   29    -1 				<a href="{{$audio.Url | html}}" tabindex="-1">
   -1    21 				<a href="/{{.CoreId | html}}/" tabindex="-1">
   30    22 					<img src="{{.Image.Url1X1 | imgWidth 120 | html}}" alt="" width="120" height="120" loading="lazy">
   31    23 				</a>
   32    24 				<div>
   33    -1 					<time datetime="{{.PublishDate | html}}">{{.PublishDate | date | html}}</time>
   34    -1 					<h3><a href="{{$audio.Url | html}}">{{.Title | html}}</a></h3>
   35    -1 					<p>{{.Summary | html}}…</p>
   -1    25 					<h3><a href="/{{.CoreId | html}}/">{{.Title | html}}</a></h3>
   -1    26 					<p>{{.Synopsis | html}}…</p>
   36    27 				</div>
   37    28 			</li>
   38    29 			{{- end}}
   39    30 		</ul>
   40    -1 		{{- if .Show.Items.PageInfo.HasNextPage}}
   41    -1 		<div class="center">
   42    -1 			<a href="?offset={{.NextOffset | html}}" rel="next" class="btn">Mehr…</a>
   43    -1 		</div>
   44    -1 		{{- end}}
   45    31 	</main>
   46    32 </body>

diff --git a/templates/show.html b/templates/show.html

@@ -8,6 +8,13 @@
    8     8 	<link rel="alternate" type="application/rss+xml" href="/{{.Id | html}}.rss">
    9     9 </head>
   10    10 <body>
   -1    11 	<header>
   -1    12 		<h1><a href="/" rel="home">Audiothek</a></h1>
   -1    13 		<form action="/search/" role="search">
   -1    14 			<input type="search" name="q">
   -1    15 			<button class="btn">Suche</button>
   -1    16 		</form>
   -1    17 	</header>
   11    18 	<main>
   12    19 		<div class="center">
   13    20 			<h2>{{.Show.Title | html}}</h2>