- 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}} · Audiothek</title>-1 6 <title>Suche · 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 · {{.Show.EditorialCategory.Title | html}}18 -1 {{- end}}19 -1 · <a href="/{{.Id | html}}.rss" rel="alternate">RSS</a>20 -1 · <a href="/{{.Id | html}}.m3u" rel="alternate">M3U</a>21 -1 · <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>