feat: add search functionality
This commit is contained in:
parent
b7c91e7f1b
commit
dd0ee8723b
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
@ -34,9 +33,7 @@ func (s *song) parseMetadata(doc *goquery.Document) {
|
|||||||
title := doc.Find("h1[class*='Title']").First().Text()
|
title := doc.Find("h1[class*='Title']").First().Text()
|
||||||
image, exists := doc.Find("meta[property='og:image']").Attr("content")
|
image, exists := doc.Find("meta[property='og:image']").Attr("content")
|
||||||
if exists {
|
if exists {
|
||||||
if u, err := url.Parse(image); err == nil {
|
s.Image = extractURL(image)
|
||||||
s.Image = fmt.Sprintf("/images%s", u.Path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Title = title
|
s.Title = title
|
||||||
|
1
main.go
1
main.go
@ -27,6 +27,7 @@ func main() {
|
|||||||
r.Use(securityHeaders)
|
r.Use(securityHeaders)
|
||||||
|
|
||||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { render("home", w, nil) })
|
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { render("home", w, nil) })
|
||||||
|
r.HandleFunc("/search", searchHandler).Methods("GET")
|
||||||
r.HandleFunc("/{id}-lyrics", lyricsHandler)
|
r.HandleFunc("/{id}-lyrics", lyricsHandler)
|
||||||
r.HandleFunc("/images/{filename}.{ext}", proxyHandler)
|
r.HandleFunc("/images/{filename}.{ext}", proxyHandler)
|
||||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||||
|
10
proxy.go
10
proxy.go
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -20,6 +21,15 @@ func isValidExt(ext string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractURL(image string) string {
|
||||||
|
u, err := url.Parse(image)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("/images%s", u.Path)
|
||||||
|
}
|
||||||
|
|
||||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
v := mux.Vars(r)
|
v := mux.Vars(r)
|
||||||
f := v["filename"]
|
f := v["filename"]
|
||||||
|
60
search.go
Normal file
60
search.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Response struct {
|
||||||
|
Sections sections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
ArtistNames string `json:"artist_names"`
|
||||||
|
Title string
|
||||||
|
Path string
|
||||||
|
Thumbnail string `json:"song_art_image_thumbnail_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hits []struct {
|
||||||
|
Result result
|
||||||
|
}
|
||||||
|
|
||||||
|
type sections []struct {
|
||||||
|
Type string
|
||||||
|
Hits hits
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderVars struct {
|
||||||
|
Query string
|
||||||
|
Sections sections
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query().Get("q")
|
||||||
|
url := fmt.Sprintf(`https://genius.com/api/search/multi?q=%s`, query)
|
||||||
|
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorln(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
render("error", w, map[string]string{
|
||||||
|
"Status": "500",
|
||||||
|
"Error": "cannot reach genius servers",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
var data response
|
||||||
|
|
||||||
|
d := json.NewDecoder(res.Body)
|
||||||
|
d.Decode(&data)
|
||||||
|
|
||||||
|
vars := renderVars{query, data.Response.Sections}
|
||||||
|
|
||||||
|
render("search", w, vars)
|
||||||
|
}
|
@ -247,6 +247,70 @@ footer a:hover {
|
|||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3rem;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 800px) {
|
||||||
|
#search-page {
|
||||||
|
width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-results h1 {
|
||||||
|
text-align:center;
|
||||||
|
color: #111;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item {
|
||||||
|
display: flex;
|
||||||
|
height: 8rem;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 1px 1px #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item span {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item img {
|
||||||
|
width: 8rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* dark mode */
|
/* dark mode */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body {
|
body {
|
||||||
@ -287,4 +351,24 @@ footer a:hover {
|
|||||||
#home p, #error p{
|
#home p, #error p{
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search-input {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-page h1 {
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item {
|
||||||
|
border: 1px solid #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item h2 {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-item span {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
utils.go
3
utils.go
@ -62,7 +62,8 @@ func getTemplates(templates ...string) []string {
|
|||||||
|
|
||||||
func render(n string, w http.ResponseWriter, data interface{}) {
|
func render(n string, w http.ResponseWriter, data interface{}) {
|
||||||
w.Header().Set("content-type", "text/html")
|
w.Header().Set("content-type", "text/html")
|
||||||
t, err := template.ParseFiles(getTemplates(n, "navbar", "footer")...)
|
t := template.New(n + ".tmpl").Funcs(template.FuncMap{"extractURL": extractURL})
|
||||||
|
t, err := t.ParseFiles(getTemplates(n, "navbar", "footer")...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorln(err)
|
logger.Errorln(err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
<h1>Welcome to dumb</h1>
|
<h1>Welcome to dumb</h1>
|
||||||
<p>An alternative frontend for genius.com</p>
|
<p>An alternative frontend for genius.com</p>
|
||||||
</div>
|
</div>
|
||||||
<code>Just redirect Genius URLs to this instance and It's all good.</code>
|
<form method="GET" action="/search">
|
||||||
|
<input type="text" name="q" id="search-input" placeholder="Search..." />
|
||||||
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{template "footer"}}
|
{{template "footer"}}
|
||||||
</div>
|
</div>
|
||||||
|
36
views/search.tmpl
Normal file
36
views/search.tmpl
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Search - dumb</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/style.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main id="app">
|
||||||
|
{{template "navbar"}}
|
||||||
|
<div id="search-page" class="main">
|
||||||
|
<form method="GET">
|
||||||
|
<input type="text" name="q" id="search-input" placeholder="Search..." value="{{.Query}}" />
|
||||||
|
</form>
|
||||||
|
<div id="search-results">
|
||||||
|
{{range .Sections}}
|
||||||
|
{{if eq .Type "song"}}
|
||||||
|
<h1>Songs</h1>
|
||||||
|
{{range .Hits}}
|
||||||
|
<a id="search-item" href="{{.Result.Path}}">
|
||||||
|
<img src="{{extractURL .Result.Thumbnail}}"/>
|
||||||
|
<div>
|
||||||
|
<span>{{.Result.ArtistNames}}</span>
|
||||||
|
<h2>{{.Result.Title}}</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "footer"}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user