diff --git a/.gitignore b/.gitignore
index b016f34..8373007 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
dumb
+views/*_templ.go
diff --git a/Dockerfile b/Dockerfile
index d063447..9e08192 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .
-RUN go build
+RUN make build
EXPOSE 5555/tcp
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..009f4d6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+gentempl:
+ @command -v templ &> /dev/null || go install github.com/a-h/templ/cmd/templ@latest
+build:gentempl
+ templ generate && go build -o dumb
diff --git a/README.md b/README.md
index 33e5248..08fef1f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ With the massive daily increase of useless scripts on Genius's web frontend and
```bash
git clone https://github.com/rramiachraf/dumb
cd dumb
-go build
+make build
./dumb
```
diff --git a/album.go b/album.go
deleted file mode 100644
index e21d8a2..0000000
--- a/album.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "strings"
-
- "github.com/PuerkitoBio/goquery"
- "github.com/gorilla/mux"
-)
-
-type album struct {
- Artist string
- Name string
- Image string
- About [2]string
-
- Tracks []Track
-}
-
-type Track struct {
- Title string
- Url string
-}
-
-type albumMetadata struct {
- Album struct {
- Id int `json:"id"`
- Image string `json:"cover_art_thumbnail_url"`
- Name string `json:"name"`
- Description string `json:"description_preview"`
- Artist `json:"artist"`
- }
- AlbumAppearances []AlbumAppearances `json:"album_appearances"`
-}
-
-type AlbumAppearances struct {
- Id int `json:"id"`
- TrackNumber int `json:"track_number"`
- Song struct {
- Title string `json:"title"`
- Url string `json:"url"`
- }
-}
-
-type Artist struct {
- Name string `json:"name"`
-}
-
-func (a *album) parseAlbumData(doc *goquery.Document) {
- pageMetadata, exists := doc.Find("meta[itemprop='page_data']").Attr("content")
- if !exists {
- return
- }
-
- var albumMetadataFromPage albumMetadata
- json.Unmarshal([]byte(pageMetadata), &albumMetadataFromPage)
-
- albumData := albumMetadataFromPage.Album
- a.Artist = albumData.Artist.Name
- a.Name = albumData.Name
- a.Image = albumData.Image
- a.About[0] = albumData.Description
- a.About[1] = truncateText(albumData.Description)
-
- for _, track := range albumMetadataFromPage.AlbumAppearances {
- url := strings.Replace(track.Song.Url, "https://genius.com", "", -1)
- a.Tracks = append(a.Tracks, Track{Title: track.Song.Title, Url: url})
- }
-}
-
-func (a *album) parse(doc *goquery.Document) {
- a.parseAlbumData(doc)
-}
-
-func albumHandler(w http.ResponseWriter, r *http.Request) {
- artist := mux.Vars(r)["artist"]
- albumName := mux.Vars(r)["albumName"]
-
- id := fmt.Sprintf("%s/%s", artist, albumName)
-
- if data, err := getCache(id); err == nil {
- render("album", w, data)
- return
- }
-
- url := fmt.Sprintf("https://genius.com/albums/%s/%s", artist, albumName)
-
- resp, err := sendRequest(url)
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "cannot reach genius servers",
- })
- return
- }
-
- defer resp.Body.Close()
-
- if resp.StatusCode == http.StatusNotFound {
- w.WriteHeader(http.StatusNotFound)
- render("error", w, map[string]string{
- "Status": "404",
- "Error": "page not found",
- })
- return
- }
-
- doc, err := goquery.NewDocumentFromReader(resp.Body)
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "something went wrong",
- })
- return
- }
-
- cf := doc.Find(".cloudflare_content").Length()
- if cf > 0 {
- logger.Errorln("cloudflare got in the way")
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "damn cloudflare, issue #21 on GitHub",
- })
- return
- }
-
- var a album
- a.parse(doc)
-
- render("album", w, a)
-
- setCache(id, a)
-}
diff --git a/annotations.go b/annotations.go
deleted file mode 100644
index f77d35c..0000000
--- a/annotations.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
- "regexp"
- "strings"
-
- "github.com/gorilla/mux"
-)
-
-type annotationsResponse struct {
- Response struct {
- Referent struct {
- Annotations []struct {
- Body struct {
- Html string `json:"html"`
- } `json:"body"`
- } `json:"annotations"`
- } `json:"referent"`
- } `json:"response"`
-}
-
-func annotationsHandler(w http.ResponseWriter, r *http.Request) {
- id := mux.Vars(r)["id"]
-
- if data, err := getCache(id); err == nil {
-
- response, err := json.Marshal(data)
-
- if err != nil {
- logger.Errorf("could not marshal json: %s\n", err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "Could not parse genius api response",
- })
- return
- }
- w.Header().Set("content-type", "application/json")
- _, err = w.Write(response)
- if err != nil {
- logger.Errorln("Error sending response: ", err)
- }
- return
- }
-
- url := fmt.Sprintf("https://genius.com/api/referents/%s?text_format=html", id)
- resp, err := sendRequest(url)
-
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "cannot reach genius servers",
- })
- return
- }
-
- defer resp.Body.Close()
-
- if resp.StatusCode == http.StatusNotFound {
- w.WriteHeader(http.StatusNotFound)
- render("error", w, map[string]string{
- "Status": "404",
- "Error": "page not found",
- })
- return
- }
-
- buf := new(bytes.Buffer)
- _, err = buf.ReadFrom(resp.Body)
- if err != nil {
- logger.Errorln("Error paring genius api response", err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "Parsing error",
- })
- return
- }
-
- var data annotationsResponse
- err = json.Unmarshal(buf.Bytes(), &data)
- if err != nil {
- logger.Errorf("could not unmarshal json: %s\n", err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "Could not parse genius api response",
- })
- return
- }
-
- w.Header().Set("content-type", "application/json")
- body := data.Response.Referent.Annotations[0].Body
- body.Html = cleanBody(body.Html)
- response, err := json.Marshal(body)
-
- if err != nil {
- logger.Errorf("could not marshal json: %s\n", err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "Could not parse genius api response",
- })
- return
- }
-
- setCache(id, body)
- _, err = w.Write(response)
- if err != nil {
- logger.Errorln("Error sending response: ", err)
- }
-}
-
-func cleanBody(body string) string {
- var withCleanedImageLinks = strings.Replace(body, "https://images.rapgenius.com/", "/images/", -1)
-
- var re = regexp.MustCompile(`https?:\/\/[a-z]*.?genius.com`)
- var withCleanedLinks = re.ReplaceAllString(withCleanedImageLinks, "")
-
- return withCleanedLinks
-}
diff --git a/data/album.go b/data/album.go
new file mode 100644
index 0000000..775f7bb
--- /dev/null
+++ b/data/album.go
@@ -0,0 +1,74 @@
+package data
+
+import (
+ "encoding/json"
+ "strings"
+
+ "github.com/PuerkitoBio/goquery"
+)
+
+type Album struct {
+ Artist string
+ Name string
+ Image string
+ About [2]string
+
+ Tracks []Track
+}
+
+type Track struct {
+ Title string
+ Url string
+}
+
+type albumMetadata struct {
+ Album struct {
+ Id int `json:"id"`
+ Image string `json:"cover_art_thumbnail_url"`
+ Name string `json:"name"`
+ Description string `json:"description_preview"`
+ Artist `json:"artist"`
+ }
+ AlbumAppearances []AlbumAppearances `json:"album_appearances"`
+}
+
+type AlbumAppearances struct {
+ Id int `json:"id"`
+ TrackNumber int `json:"track_number"`
+ Song struct {
+ Title string `json:"title"`
+ Url string `json:"url"`
+ }
+}
+
+type Artist struct {
+ Name string `json:"name"`
+}
+
+func (a *Album) parseAlbumData(doc *goquery.Document) {
+ pageMetadata, exists := doc.Find("meta[itemprop='page_data']").Attr("content")
+ if !exists {
+ return
+ }
+
+ var albumMetadataFromPage albumMetadata
+ json.Unmarshal([]byte(pageMetadata), &albumMetadataFromPage)
+
+ albumData := albumMetadataFromPage.Album
+ a.Artist = albumData.Artist.Name
+ a.Name = albumData.Name
+ a.Image = albumData.Image
+ a.About[0] = albumData.Description
+ //a.About[1] = truncateText(albumData.Description)
+ a.About[1] = ""
+
+ for _, track := range albumMetadataFromPage.AlbumAppearances {
+ url := strings.Replace(track.Song.Url, "https://genius.com", "", -1)
+ a.Tracks = append(a.Tracks, Track{Title: track.Song.Title, Url: url})
+ }
+}
+
+func (a *Album) Parse(doc *goquery.Document) {
+ a.parseAlbumData(doc)
+}
+
diff --git a/data/annotation.go b/data/annotation.go
new file mode 100644
index 0000000..70787bc
--- /dev/null
+++ b/data/annotation.go
@@ -0,0 +1,15 @@
+package data
+
+type AnnotationsResponse struct {
+ Response struct {
+ Referent struct {
+ Annotations []Annotation `json:"annotations"`
+ } `json:"referent"`
+ } `json:"response"`
+}
+
+type Annotation struct {
+ Body struct {
+ Html string `json:"html"`
+ } `json:"body"`
+}
diff --git a/lyrics.go b/data/lyrics.go
similarity index 57%
rename from lyrics.go
rename to data/lyrics.go
index 0fdcb8c..5ba8636 100644
--- a/lyrics.go
+++ b/data/lyrics.go
@@ -1,16 +1,15 @@
-package main
+package data
import (
"encoding/json"
"fmt"
- "net/http"
"strings"
"github.com/PuerkitoBio/goquery"
- "github.com/gorilla/mux"
+ "github.com/sirupsen/logrus"
)
-type song struct {
+type Song struct {
Artist string
Title string
Image string
@@ -46,17 +45,17 @@ type customPerformance struct {
}
}
-func (s *song) parseLyrics(doc *goquery.Document) {
+func (s *Song) parseLyrics(doc *goquery.Document) {
doc.Find("[data-lyrics-container='true']").Each(func(i int, ss *goquery.Selection) {
h, err := ss.Html()
if err != nil {
- logger.Errorln("unable to parse lyrics", err)
+ logrus.Errorln("unable to parse lyrics", err)
}
s.Lyrics += h
})
}
-func (s *song) parseSongData(doc *goquery.Document) {
+func (s *Song) parseSongData(doc *goquery.Document) {
attr, exists := doc.Find("meta[property='twitter:app:url:iphone']").Attr("content")
if exists {
songID := strings.Replace(attr, "genius://songs/", "", 1)
@@ -65,7 +64,7 @@ func (s *song) parseSongData(doc *goquery.Document) {
res, err := sendRequest(u)
if err != nil {
- logger.Errorln(err)
+ logrus.Errorln(err)
}
defer res.Body.Close()
@@ -74,7 +73,7 @@ func (s *song) parseSongData(doc *goquery.Document) {
decoder := json.NewDecoder(res.Body)
err = decoder.Decode(&data)
if err != nil {
- logger.Errorln(err)
+ logrus.Errorln(err)
}
songData := data.Response.Song
@@ -107,66 +106,7 @@ func truncateText(text string) string {
return text
}
-func (s *song) parse(doc *goquery.Document) {
+func (s *Song) Parse(doc *goquery.Document) {
s.parseLyrics(doc)
s.parseSongData(doc)
}
-
-func lyricsHandler(w http.ResponseWriter, r *http.Request) {
- id := mux.Vars(r)["id"]
-
- if data, err := getCache(id); err == nil {
- render("lyrics", w, data)
- return
- }
-
- url := fmt.Sprintf("https://genius.com/%s-lyrics", id)
- resp, err := sendRequest(url)
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "cannot reach genius servers",
- })
- return
- }
-
- defer resp.Body.Close()
-
- if resp.StatusCode == http.StatusNotFound {
- w.WriteHeader(http.StatusNotFound)
- render("error", w, map[string]string{
- "Status": "404",
- "Error": "page not found",
- })
- return
- }
-
- doc, err := goquery.NewDocumentFromReader(resp.Body)
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "something went wrong",
- })
- return
- }
-
- cf := doc.Find(".cloudflare_content").Length()
- if cf > 0 {
- logger.Errorln("cloudflare got in the way")
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "damn cloudflare, issue #21 on GitHub",
- })
- return
- }
-
- var s song
- s.parse(doc)
-
- render("lyrics", w, s)
- setCache(id, s)
-}
diff --git a/data/proxy.go b/data/proxy.go
new file mode 100644
index 0000000..4b432d8
--- /dev/null
+++ b/data/proxy.go
@@ -0,0 +1,16 @@
+package data
+
+import (
+ "fmt"
+ "net/url"
+)
+
+func ExtractImageURL(image string) string {
+ u, err := url.Parse(image)
+ if err != nil {
+ return ""
+ }
+
+ return fmt.Sprintf("/images%s", u.Path)
+}
+
diff --git a/data/search.go b/data/search.go
new file mode 100644
index 0000000..d78ce41
--- /dev/null
+++ b/data/search.go
@@ -0,0 +1,28 @@
+package data
+
+type SearchResponse 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 SearchResults struct {
+ Query string
+ Sections sections
+}
diff --git a/data/utils.go b/data/utils.go
new file mode 100644
index 0000000..022533a
--- /dev/null
+++ b/data/utils.go
@@ -0,0 +1,46 @@
+package data
+
+import (
+ "net"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/caffix/cloudflare-roundtripper/cfrt"
+)
+
+const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
+
+var client = &http.Client{
+ Timeout: 20 * time.Second,
+ Transport: &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 15 * time.Second,
+ KeepAlive: 15 * time.Second,
+ DualStack: true,
+ }).DialContext,
+ },
+}
+
+func sendRequest(u string) (*http.Response, error) {
+ url, err := url.Parse(u)
+ if err != nil {
+ return nil, err
+ }
+
+ client.Transport, err = cfrt.New(client.Transport)
+ if err != nil {
+ return nil, err
+ }
+
+ req := &http.Request{
+ Method: http.MethodGet,
+ URL: url,
+ Header: map[string][]string{
+ "Accept-Language": {"en-US"},
+ "User-Agent": {UA},
+ },
+ }
+
+ return client.Do(req)
+}
diff --git a/go.mod b/go.mod
index 6b46600..8455ef0 100644
--- a/go.mod
+++ b/go.mod
@@ -1,20 +1,23 @@
module github.com/rramiachraf/dumb
-go 1.18
+go 1.21
+
+toolchain go1.22.0
require (
- github.com/PuerkitoBio/goquery v1.8.0
+ github.com/PuerkitoBio/goquery v1.8.1
+ github.com/a-h/templ v0.2.598
github.com/allegro/bigcache/v3 v3.0.2
+ github.com/caffix/cloudflare-roundtripper v0.0.0-20181218223503-4c29d231c9cb
github.com/gorilla/mux v1.8.0
github.com/sirupsen/logrus v1.9.3
)
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
- github.com/caffix/cloudflare-roundtripper v0.0.0-20181218223503-4c29d231c9cb // indirect
github.com/robertkrimen/otto v0.2.1 // indirect
- golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 // indirect
- golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
- golang.org/x/text v0.4.0 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
)
diff --git a/go.sum b/go.sum
index 3eb7ee1..101c6db 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
-github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
-github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
+github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
+github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
+github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo=
+github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
github.com/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9IrprcQfI=
github.com/allegro/bigcache/v3 v3.0.2/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
@@ -9,35 +11,60 @@ github.com/caffix/cloudflare-roundtripper v0.0.0-20181218223503-4c29d231c9cb/go.
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/handlers/album.go b/handlers/album.go
new file mode 100644
index 0000000..bbdca54
--- /dev/null
+++ b/handlers/album.go
@@ -0,0 +1,67 @@
+package handlers
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/gorilla/mux"
+ "github.com/rramiachraf/dumb/data"
+ "github.com/rramiachraf/dumb/views"
+ "github.com/sirupsen/logrus"
+)
+
+func Album(l *logrus.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ artist := mux.Vars(r)["artist"]
+ albumName := mux.Vars(r)["albumName"]
+
+ id := fmt.Sprintf("%s/%s", artist, albumName)
+
+ if a, err := getCache[data.Album](id); err == nil {
+ views.AlbumPage(a).Render(context.Background(), w)
+ return
+ }
+
+ url := fmt.Sprintf("https://genius.com/albums/%s/%s", artist, albumName)
+
+ resp, err := sendRequest(url)
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "cannot reach Genius servers").Render(context.Background(), w)
+ return
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ w.WriteHeader(http.StatusNotFound)
+ views.ErrorPage(404, "page not found").Render(context.Background(), w)
+ return
+ }
+
+ doc, err := goquery.NewDocumentFromReader(resp.Body)
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ cf := doc.Find(".cloudflare_content").Length()
+ if cf > 0 {
+ l.Errorln("cloudflare got in the way")
+ views.ErrorPage(500, "i'll fix this later #21").Render(context.Background(), w)
+ return
+ }
+
+ var a data.Album
+ a.Parse(doc)
+
+ views.AlbumPage(a).Render(context.Background(), w)
+
+ setCache(id, a)
+ }
+}
diff --git a/handlers/annotations.go b/handlers/annotations.go
new file mode 100644
index 0000000..a12df8f
--- /dev/null
+++ b/handlers/annotations.go
@@ -0,0 +1,103 @@
+package handlers
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+
+ "github.com/gorilla/mux"
+ "github.com/rramiachraf/dumb/data"
+ "github.com/rramiachraf/dumb/views"
+ "github.com/sirupsen/logrus"
+)
+
+func Annotations(l *logrus.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ id := mux.Vars(r)["id"]
+
+ if data, err := getCache[data.Annotation](id); err == nil {
+
+ response, err := json.Marshal(data)
+
+ if err != nil {
+ l.Errorf("could not marshal json: %s\n", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+ w.Header().Set("content-type", "application/json")
+ _, err = w.Write(response)
+ if err != nil {
+ l.Errorln("Error sending response: ", err)
+ }
+ return
+ }
+
+ url := fmt.Sprintf("https://genius.com/api/referents/%s?text_format=html", id)
+ resp, err := sendRequest(url)
+
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "cannot reach genius servers").Render(context.Background(), w)
+ return
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ w.WriteHeader(http.StatusNotFound)
+ views.ErrorPage(404, "page not found").Render(context.Background(), w)
+ return
+ }
+
+ buf := new(bytes.Buffer)
+ _, err = buf.ReadFrom(resp.Body)
+ if err != nil {
+ l.Errorln("Error paring genius api response", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ var data data.AnnotationsResponse
+ err = json.Unmarshal(buf.Bytes(), &data)
+ if err != nil {
+ l.Errorf("could not unmarshal json: %s\n", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ w.Header().Set("content-type", "application/json")
+ body := data.Response.Referent.Annotations[0].Body
+ body.Html = cleanBody(body.Html)
+ response, err := json.Marshal(body)
+
+ if err != nil {
+ l.Errorf("could not marshal json: %s\n", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ setCache(id, body)
+ _, err = w.Write(response)
+ if err != nil {
+ l.Errorln("Error sending response: ", err)
+ }
+ }
+}
+
+func cleanBody(body string) string {
+ var withCleanedImageLinks = strings.Replace(body, "https://images.rapgenius.com/", "/images/", -1)
+
+ var re = regexp.MustCompile(`https?:\/\/[a-z]*.?genius.com`)
+ var withCleanedLinks = re.ReplaceAllString(withCleanedImageLinks, "")
+
+ return withCleanedLinks
+}
diff --git a/handlers/cache.go b/handlers/cache.go
new file mode 100644
index 0000000..f0d718e
--- /dev/null
+++ b/handlers/cache.go
@@ -0,0 +1,35 @@
+package handlers
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/allegro/bigcache/v3"
+ "github.com/rramiachraf/dumb/data"
+)
+
+var c, _ = bigcache.NewBigCache(bigcache.DefaultConfig(time.Hour * 24))
+
+func setCache(key string, entry interface{}) error {
+ data, err := json.Marshal(&entry)
+ if err != nil {
+ return err
+ }
+
+ return c.Set(key, data)
+}
+
+func getCache[v data.Album | data.Song | data.Annotation](key string) (v, error) {
+ var decoded v
+
+ data, err := c.Get(key)
+ if err != nil {
+ return decoded, err
+ }
+
+ if err = json.Unmarshal(data, &decoded); err != nil {
+ return decoded, err
+ }
+
+ return decoded, nil
+}
diff --git a/handlers/lyrics.go b/handlers/lyrics.go
new file mode 100644
index 0000000..3e2ee4b
--- /dev/null
+++ b/handlers/lyrics.go
@@ -0,0 +1,62 @@
+package handlers
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/gorilla/mux"
+ "github.com/rramiachraf/dumb/data"
+ "github.com/rramiachraf/dumb/views"
+ "github.com/sirupsen/logrus"
+)
+
+func Lyrics(l *logrus.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ id := mux.Vars(r)["id"]
+
+ if s, err := getCache[data.Song](id); err == nil {
+ views.LyricsPage(s).Render(context.Background(), w)
+ return
+ }
+
+ url := fmt.Sprintf("https://genius.com/%s-lyrics", id)
+ resp, err := sendRequest(url)
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "cannot reach Genius servers").Render(context.Background(), w)
+ return
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ w.WriteHeader(http.StatusNotFound)
+ views.ErrorPage(404, "page not found").Render(context.Background(), w)
+ return
+ }
+
+ doc, err := goquery.NewDocumentFromReader(resp.Body)
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ cf := doc.Find(".cloudflare_content").Length()
+ if cf > 0 {
+ l.Errorln("cloudflare got in the way")
+ views.ErrorPage(500, "TODO: fix Cloudflare #21").Render(context.Background(), w)
+ return
+ }
+
+ var s data.Song
+ s.Parse(doc)
+
+ views.LyricsPage(s).Render(context.Background(), w)
+ setCache(id, s)
+ }
+}
diff --git a/handlers/proxy.go b/handlers/proxy.go
new file mode 100644
index 0000000..ea3003d
--- /dev/null
+++ b/handlers/proxy.go
@@ -0,0 +1,58 @@
+package handlers
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/gorilla/mux"
+ "github.com/rramiachraf/dumb/views"
+ "github.com/sirupsen/logrus"
+)
+
+func isValidExt(ext string) bool {
+ valid := []string{"jpg", "jpeg", "png", "gif"}
+ for _, c := range valid {
+ if strings.ToLower(ext) == c {
+ return true
+ }
+ }
+
+ return false
+}
+
+func ImageProxy(l *logrus.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ v := mux.Vars(r)
+ f := v["filename"]
+ ext := v["ext"]
+
+ if !isValidExt(ext) {
+ w.WriteHeader(http.StatusBadRequest)
+ views.ErrorPage(400, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ // first segment of URL resize the image to reduce bandwith usage.
+ url := fmt.Sprintf("https://t2.genius.com/unsafe/300x300/https://images.genius.com/%s.%s", f, ext)
+
+ res, err := sendRequest(url)
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "cannot reach Genius servers").Render(context.Background(), w)
+ return
+ }
+
+ if res.StatusCode != http.StatusOK {
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "something went wrong").Render(context.Background(), w)
+ return
+ }
+
+ w.Header().Add("Content-type", fmt.Sprintf("image/%s", ext))
+ io.Copy(w, res.Body)
+ }
+}
diff --git a/handlers/search.go b/handlers/search.go
new file mode 100644
index 0000000..26265d9
--- /dev/null
+++ b/handlers/search.go
@@ -0,0 +1,40 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/rramiachraf/dumb/data"
+ "github.com/rramiachraf/dumb/views"
+ "github.com/sirupsen/logrus"
+)
+
+func Search(l *logrus.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query().Get("q")
+ url := fmt.Sprintf(`https://genius.com/api/search/multi?q=%s`, url.QueryEscape(query))
+
+ res, err := sendRequest(url)
+ if err != nil {
+ l.Errorln(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ views.ErrorPage(500, "cannot reach Genius servers").Render(context.Background(), w)
+ return
+ }
+
+ defer res.Body.Close()
+
+ var sRes data.SearchResponse
+
+ d := json.NewDecoder(res.Body)
+ d.Decode(&sRes)
+
+ results := data.SearchResults{Query: query, Sections: sRes.Response.Sections}
+
+ views.SearchPage(results).Render(context.Background(), w)
+ }
+
+}
diff --git a/handlers/utils.go b/handlers/utils.go
new file mode 100644
index 0000000..8eeb669
--- /dev/null
+++ b/handlers/utils.go
@@ -0,0 +1,49 @@
+package handlers
+
+import (
+ "net"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+func MustHeaders(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ csp := "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'"
+ w.Header().Add("content-security-policy", csp)
+ w.Header().Add("referrer-policy", "no-referrer")
+ w.Header().Add("x-content-type-options", "nosniff")
+ next.ServeHTTP(w, r)
+ })
+}
+
+const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
+
+var client = &http.Client{
+ Timeout: 20 * time.Second,
+ Transport: &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 15 * time.Second,
+ KeepAlive: 15 * time.Second,
+ DualStack: true,
+ }).DialContext,
+ },
+}
+
+func sendRequest(u string) (*http.Response, error) {
+ url, err := url.Parse(u)
+ if err != nil {
+ return nil, err
+ }
+
+ req := &http.Request{
+ Method: http.MethodGet,
+ URL: url,
+ Header: map[string][]string{
+ "Accept-Language": {"en-US"},
+ "User-Agent": {UA},
+ },
+ }
+
+ return client.Do(req)
+}
diff --git a/main.go b/main.go
index 69799f9..39c9c21 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "context"
"fmt"
"net"
"net/http"
@@ -8,38 +9,30 @@ import (
"strconv"
"time"
- "github.com/allegro/bigcache/v3"
+ "github.com/a-h/templ"
"github.com/gorilla/mux"
+ "github.com/rramiachraf/dumb/handlers"
+ "github.com/rramiachraf/dumb/views"
"github.com/sirupsen/logrus"
)
var logger = logrus.New()
func main() {
- c, err := bigcache.NewBigCache(bigcache.DefaultConfig(time.Hour * 24))
- if err != nil {
- logger.Fatalln("can't initialize caching")
- }
- cache = c
-
r := mux.NewRouter()
- r.Use(securityHeaders)
+ r.Use(handlers.MustHeaders)
- 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}/{artist-song}/{verse}/annotations", annotationsHandler)
- r.HandleFunc("/images/{filename}.{ext}", proxyHandler)
+ r.Handle("/", templ.Handler(views.HomePage()))
+ r.HandleFunc("/{id}-lyrics", handlers.Lyrics(logger)).Methods("GET")
+ r.HandleFunc("/albums/{artist}/{albumName}", handlers.Album(logger)).Methods("GET")
+ r.HandleFunc("/images/{filename}.{ext}", handlers.ImageProxy(logger)).Methods("GET")
+ r.HandleFunc("/search", handlers.Search(logger)).Methods("GET")
+ r.HandleFunc("/{id}/{artist-song}/{verse}/annotations", handlers.Annotations(logger)).Methods("GET")
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
- r.HandleFunc("/albums/{artist}/{albumName}", albumHandler)
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
- render("error", w, map[string]string{
- "Status": "404",
- "Error": "page not found",
- })
-
+ views.ErrorPage(404, "page not found").Render(context.Background(), w)
})
server := &http.Server{
diff --git a/proxy.go b/proxy.go
deleted file mode 100644
index 006c932..0000000
--- a/proxy.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package main
-
-import (
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
-
- "github.com/gorilla/mux"
-)
-
-func isValidExt(ext string) bool {
- valid := []string{"jpg", "jpeg", "png", "gif"}
- for _, c := range valid {
- if strings.ToLower(ext) == c {
- return true
- }
- }
-
- 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) {
- v := mux.Vars(r)
- f := v["filename"]
- ext := v["ext"]
-
- if !isValidExt(ext) {
- w.WriteHeader(http.StatusBadRequest)
- render("error", w, map[string]string{
- "Status": "400",
- "Error": "Something went wrong",
- })
- return
- }
-
- // first segment of URL resize the image to reduce bandwith usage.
- url := fmt.Sprintf("https://t2.genius.com/unsafe/300x300/https://images.genius.com/%s.%s", f, ext)
-
- res, err := sendRequest(url)
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "cannot reach genius servers",
- })
- return
- }
-
- if res.StatusCode != http.StatusOK {
- w.WriteHeader(http.StatusInternalServerError)
- render("error", w, map[string]string{
- "Status": "500",
- "Error": "something went wrong",
- })
-
- return
- }
-
- w.Header().Add("Content-type", fmt.Sprintf("image/%s", ext))
- io.Copy(w, res.Body)
-}
diff --git a/scripts/dumb.service b/scripts/dumb.service
deleted file mode 100644
index 09d242f..0000000
--- a/scripts/dumb.service
+++ /dev/null
@@ -1,21 +0,0 @@
-[Unit]
-Description=ListMonk
-Documentation=https://github.com/rramiachraf/dumb
-After=system.slice multi-user.target postgresql.service network.target
-
-[Service]
-User=git
-Type=simple
-
-StandardOutput=syslog
-StandardError=syslog
-SyslogIdentifier=listmonk
-
-WorkingDirectory=/etc/dumb
-ExecStart=/etc/dumb/dumb
-
-Restart=always
-RestartSec=5
-
-[Install]
-WantedBy=multi-user.target
diff --git a/scripts/nginx-config b/scripts/nginx-config
deleted file mode 100644
index 7cafb9d..0000000
--- a/scripts/nginx-config
+++ /dev/null
@@ -1,35 +0,0 @@
-server {
- # root /var/www/dumb.yoursite.com/html;
- # index index.html index.htm index.nginx-debian.html;
-
- server_name dumb.yoursite.com;
- # www.dumb.yoursite.com;
-
- location / {
- try_files $uri $uri/ =404;
- proxy_pass http://localhost:5555;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-
- listen 443 ssl; # managed by Certbot
- ssl_certificate /etc/letsencrypt/live/dumb.yoursite.com/fullchain.pem; # managed by Certbot
- ssl_certificate_key /etc/letsencrypt/live/dumb.yoursite.com/privkey.pem; # managed by Certbot
- include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
-
-}
-server {
- if ($host = dumb.yoursite.com) {
- return 301 https://$host$request_uri;
- } # managed by Certbot
-
- server_name dumb.yoursite.com;
-
- listen 80;
- return 404; # managed by Certbot
-
-}
-
diff --git a/search.go b/search.go
deleted file mode 100644
index 1392e8d..0000000
--- a/search.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "net/url"
-)
-
-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`, url.QueryEscape(query))
-
- res, err := sendRequest(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)
-}
diff --git a/utils.go b/utils.go
deleted file mode 100644
index 94a686a..0000000
--- a/utils.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "net"
- "net/http"
- "net/url"
- "path"
- "text/template"
- "time"
-
- "github.com/allegro/bigcache/v3"
- "github.com/caffix/cloudflare-roundtripper/cfrt"
-)
-
-var cache *bigcache.BigCache
-
-func setCache(key string, entry interface{}) error {
- data, err := json.Marshal(&entry)
- if err != nil {
- return err
- }
-
- return cache.Set(key, data)
-}
-
-func getCache(key string) (interface{}, error) {
- data, err := cache.Get(key)
- if err != nil {
- return nil, err
- }
-
- var decoded interface{}
-
- if err = json.Unmarshal(data, &decoded); err != nil {
- return nil, err
- }
-
- return decoded, nil
-}
-
-func write(w http.ResponseWriter, status int, data []byte) {
- w.WriteHeader(status)
- w.Write(data)
-}
-
-func securityHeaders(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- csp := "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'"
- w.Header().Add("content-security-policy", csp)
- w.Header().Add("referrer-policy", "no-referrer")
- w.Header().Add("x-content-type-options", "nosniff")
- next.ServeHTTP(w, r)
- })
-}
-
-func getTemplates(templates ...string) []string {
- var pths []string
- for _, t := range templates {
- tmpl := path.Join("views", fmt.Sprintf("%s.tmpl", t))
- pths = append(pths, tmpl)
- }
- return pths
-}
-
-func render(n string, w http.ResponseWriter, data interface{}) {
- w.Header().Set("content-type", "text/html")
- t := template.New(n + ".tmpl").Funcs(template.FuncMap{"extractURL": extractURL})
- t, err := t.ParseFiles(getTemplates(n, "navbar", "footer")...)
- if err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err = t.Execute(w, data); err != nil {
- logger.Errorln(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-}
-
-const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
-
-var client = &http.Client{
- Timeout: 20 * time.Second,
- Transport: &http.Transport{
- DialContext: (&net.Dialer{
- Timeout: 15 * time.Second,
- KeepAlive: 15 * time.Second,
- DualStack: true,
- }).DialContext,
- },
-}
-
-func sendRequest(u string) (*http.Response, error) {
- url, err := url.Parse(u)
- if err != nil {
- return nil, err
- }
-
- client.Transport, err = cfrt.New(client.Transport)
- if err != nil {
- return nil, err
- }
-
-
- req := &http.Request{
- Method: http.MethodGet,
- URL: url,
- Header: map[string][]string{
- "Accept-Language": {"en-US"},
- "User-Agent": {UA},
- },
- }
-
- return client.Do(req)
-}
diff --git a/views/album.templ b/views/album.templ
new file mode 100644
index 0000000..5e61a04
--- /dev/null
+++ b/views/album.templ
@@ -0,0 +1,32 @@
+package views
+
+import (
+ "fmt"
+ "github.com/rramiachraf/dumb/data"
+)
+
+templ AlbumPage(a data.Album) {
+ @layout(fmt.Sprintf("%s - %s", a.Artist, a.Name)) {
+
+
+
+
+
+
About
+
{ a.About[0] }
+
{ a.About[1] }
+
+
+
+ }
+}
diff --git a/views/album.tmpl b/views/album.tmpl
deleted file mode 100644
index db35d77..0000000
--- a/views/album.tmpl
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- {{.Artist}} - {{.Name}}
-
-
-
-
-
-
- {{template "navbar"}}
-
-
-
-
-
-
About
-
{{index .About 0}}
-
{{index .About 1}}
-
-
-
- {{template "footer"}}
-
-
diff --git a/views/error.templ b/views/error.templ
new file mode 100644
index 0000000..7bfe3ec
--- /dev/null
+++ b/views/error.templ
@@ -0,0 +1,12 @@
+package views
+
+import "strconv"
+
+templ ErrorPage(code int, display string) {
+ @layout("Error - dumb") {
+
+
{ strconv.Itoa(code) }
+
{ display }
+
+ }
+}
diff --git a/views/error.tmpl b/views/error.tmpl
deleted file mode 100644
index 39e0729..0000000
--- a/views/error.tmpl
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
- dumb
-
-
-
-
-
-
-
- {{template "navbar"}}
-
-
{{.Status}}
-
{{.Error}}
-
- {{template "footer"}}
-
-
-
diff --git a/views/footer.templ b/views/footer.templ
new file mode 100644
index 0000000..1701078
--- /dev/null
+++ b/views/footer.templ
@@ -0,0 +1,7 @@
+package views
+
+templ footer() {
+
+}
diff --git a/views/footer.tmpl b/views/footer.tmpl
deleted file mode 100644
index 394117c..0000000
--- a/views/footer.tmpl
+++ /dev/null
@@ -1,5 +0,0 @@
-{{define "footer"}}
-
-{{end}}
diff --git a/views/head.templ b/views/head.templ
new file mode 100644
index 0000000..f82ffd1
--- /dev/null
+++ b/views/head.templ
@@ -0,0 +1,12 @@
+package views
+
+templ head(title string) {
+
+ { title }
+
+
+
+
+
+
+}
diff --git a/views/home.templ b/views/home.templ
new file mode 100644
index 0000000..3890c20
--- /dev/null
+++ b/views/home.templ
@@ -0,0 +1,15 @@
+package views
+
+templ HomePage() {
+ @layout("dumb") {
+
+
+
Welcome to dumb
+
An alternative frontend for genius.com
+
+
+
+ }
+}
diff --git a/views/home.tmpl b/views/home.tmpl
deleted file mode 100644
index 0b3397e..0000000
--- a/views/home.tmpl
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- dumb
-
-
-
-
-
-
-
- {{template "navbar"}}
-
-
-
Welcome to dumb
-
An alternative frontend for genius.com
-
-
-
-
- {{template "footer"}}
-
-
-
diff --git a/views/layout.templ b/views/layout.templ
new file mode 100644
index 0000000..b02da33
--- /dev/null
+++ b/views/layout.templ
@@ -0,0 +1,15 @@
+package views
+
+templ layout(title string) {
+
+
+ @head(title)
+
+
+ @navbar()
+ { children... }
+ @footer()
+
+
+
+}
diff --git a/views/lyrics.templ b/views/lyrics.templ
new file mode 100644
index 0000000..73806e0
--- /dev/null
+++ b/views/lyrics.templ
@@ -0,0 +1,38 @@
+package views
+
+import (
+ "fmt"
+ "github.com/rramiachraf/dumb/data"
+)
+
+templ LyricsPage(s data.Song) {
+ @layout(fmt.Sprintf("%s - %s lyrics", s.Artist, s.Title)) {
+
+
+
+ @templ.Raw(s.Lyrics)
+
+
+
+
About
+
{ s.About[0] }
+
{ s.About[1] }
+
+
+
Credits
+ for key, val := range s.Credits {
+
+ { key }
+ { val }
+
+ }
+
+
+
+ }
+}
diff --git a/views/lyrics.tmpl b/views/lyrics.tmpl
deleted file mode 100644
index 5b9505d..0000000
--- a/views/lyrics.tmpl
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- {{.Artist}} - {{.Title}} lyrics
-
-
-
-
-
-
-
- {{template "navbar"}}
-
-
-
{{.Lyrics}}
-
-
-
About
-
{{index .About 0}}
-
{{index .About 1}}
-
-
-
Credits
- {{range $key, $val := .Credits}}
-
- {{$key}}
- {{$val}}
-
- {{end}}
-
-
-
- {{template "footer"}}
-
-
diff --git a/views/navbar.templ b/views/navbar.templ
new file mode 100644
index 0000000..66c7255
--- /dev/null
+++ b/views/navbar.templ
@@ -0,0 +1,7 @@
+package views
+
+templ navbar() {
+
+}
diff --git a/views/navbar.tmpl b/views/navbar.tmpl
deleted file mode 100644
index bf117cd..0000000
--- a/views/navbar.tmpl
+++ /dev/null
@@ -1,5 +0,0 @@
-{{define "navbar"}}
-
-{{end}}
diff --git a/views/search.templ b/views/search.templ
new file mode 100644
index 0000000..9a48b4e
--- /dev/null
+++ b/views/search.templ
@@ -0,0 +1,29 @@
+package views
+
+import "github.com/rramiachraf/dumb/data"
+
+templ SearchPage(r data.SearchResults) {
+ @layout("Search - dumb") {
+
+ }
+}
diff --git a/views/search.tmpl b/views/search.tmpl
deleted file mode 100644
index eecea1b..0000000
--- a/views/search.tmpl
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
- Search - dumb
-
-
-
-
-
-
-
- {{template "navbar"}}
-
- {{template "footer"}}
-
-
-