feat: replace text/template with templ and refactor code

This commit is contained in:
rramiachraf 2024-03-04 14:59:47 +01:00
parent 5390a2878d
commit e2d5ef044b
43 changed files with 836 additions and 851 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
dumb
views/*_templ.go

View file

@ -6,7 +6,7 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build
RUN make build
EXPOSE 5555/tcp

4
Makefile Normal file
View file

@ -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

View file

@ -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
```

139
album.go
View file

@ -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)
}

View file

@ -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
}

74
data/album.go Normal file
View file

@ -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)
}

15
data/annotation.go Normal file
View file

@ -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"`
}

View file

@ -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)
}

16
data/proxy.go Normal file
View file

@ -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)
}

28
data/search.go Normal file
View file

@ -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
}

46
data/utils.go Normal file
View file

@ -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)
}

15
go.mod
View file

@ -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
)

51
go.sum
View file

@ -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=

67
handlers/album.go Normal file
View file

@ -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)
}
}

103
handlers/annotations.go Normal file
View file

@ -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
}

35
handlers/cache.go Normal file
View file

@ -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
}

62
handlers/lyrics.go Normal file
View file

@ -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)
}
}

58
handlers/proxy.go Normal file
View file

@ -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)
}
}

40
handlers/search.go Normal file
View file

@ -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)
}
}

49
handlers/utils.go Normal file
View file

@ -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)
}

31
main.go
View file

@ -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{

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

119
utils.go
View file

@ -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)
}

32
views/album.templ Normal file
View file

@ -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)) {
<div id="container">
<div id="metadata">
<img id="album-artwork" src={ data.ExtractImageURL(a.Image) }/>
<h2>{ a.Artist }</h2>
<h1>{ a.Name }</h1>
</div>
<div id="album-tracklist">
for _, t := range a.Tracks {
<a href={ templ.URL(t.Url) }>
<p>{ t.Title }</p>
</a>
}
</div>
<div id="info">
<div id="about">
<h1 id="title">About</h1>
<p class="hidden" id="full_about">{ a.About[0] }</p>
<p id="summary">{ a.About[1] }</p>
</div>
</div>
</div>
}
}

View file

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Artist}} - {{.Name}}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/static/style.css" />
<script type="text/javascript" src="/static/script.js" defer></script>
</head>
<body>
{{template "navbar"}}
<div id="container">
<div id="metadata">
<img id="album-artwork" src="{{extractURL .Image}}"/>
<h2>{{.Artist}}</h2>
<h1>{{.Name}}</h1>
</div>
<div id="album-tracklist">
{{range .Tracks}}
<a href="{{.Url}}">
<p>{{.Title}}</p>
</a>
{{end}}
</div>
<div id="info">
<div id="about">
<h1 id="title">About</h1>
<p class="hidden" id="full_about">{{index .About 0}}</p>
<p id="summary">{{index .About 1}}</p>
</div>
</div>
</div>
{{template "footer"}}
</body>
</html>

12
views/error.templ Normal file
View file

@ -0,0 +1,12 @@
package views
import "strconv"
templ ErrorPage(code int, display string) {
@layout("Error - dumb") {
<div id="error">
<h1>{ strconv.Itoa(code) }</h1>
<p>{ display }</p>
</div>
}
}

View file

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>dumb</title>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/static/style.css" />
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<main id="app">
{{template "navbar"}}
<div id="error">
<h1>{{.Status}}</h1>
<p>{{.Error}}</p>
</div>
{{template "footer"}}
</div>
</body>
</html>

7
views/footer.templ Normal file
View file

@ -0,0 +1,7 @@
package views
templ footer() {
<footer>
<a rel="noopener noreferrer" target="_blank" href="https://github.com/rramiachraf/dumb">Source Code</a>
</footer>
}

View file

@ -1,5 +0,0 @@
{{define "footer"}}
<footer>
<a rel="noopener noreferrer" target="_blank" href="https://github.com/rramiachraf/dumb">Source Code</a>
</footer>
{{end}}

12
views/head.templ Normal file
View file

@ -0,0 +1,12 @@
package views
templ head(title string) {
<head>
<title>{ title }</title>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="/static/style.css"/>
<link rel="icon" href="/static/logo.svg" type="image/svg+xml"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="text/javascript" src="/static/script.js" defer></script>
</head>
}

15
views/home.templ Normal file
View file

@ -0,0 +1,15 @@
package views
templ HomePage() {
@layout("dumb") {
<div id="home">
<div>
<h1>Welcome to dumb</h1>
<p>An alternative frontend for genius.com</p>
</div>
<form method="GET" action="/search">
<input type="text" name="q" id="search-input" placeholder="Search..."/>
</form>
</div>
}
}

View file

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>dumb</title>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/static/style.css" />
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<main id="app">
{{template "navbar"}}
<div id="home">
<div>
<h1>Welcome to dumb</h1>
<p>An alternative frontend for genius.com</p>
</div>
<form method="GET" action="/search">
<input type="text" name="q" id="search-input" placeholder="Search..." />
</form>
</div>
{{template "footer"}}
</div>
</body>
</html>

15
views/layout.templ Normal file
View file

@ -0,0 +1,15 @@
package views
templ layout(title string) {
<!DOCTYPE html>
<html>
@head(title)
<body>
<main id="app">
@navbar()
{ children... }
@footer()
</main>
</body>
</html>
}

38
views/lyrics.templ Normal file
View file

@ -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)) {
<div id="container">
<div id="metadata">
<a href={ templ.URL(s.LinkToAlbum) }><img id="album-artwork" src={ data.ExtractImageURL(s.Image) }/></a>
<h2>{ s.Artist }</h2>
<h1>{ s.Title }</h1>
<a href={ templ.URL(s.LinkToAlbum) }><h2>{ s.Album }</h2></a>
</div>
<div id="lyrics">
@templ.Raw(s.Lyrics)
</div>
<div id="info">
<div id="about">
<h1 id="title">About</h1>
<p class="hidden" id="full_about">{ s.About[0] }</p>
<p id="summary">{ s.About[1] }</p>
</div>
<div id="credits">
<h1 id="title">Credits</h1>
for key, val := range s.Credits {
<details>
<summary>{ key }</summary>
<p>{ val }</p>
</details>
}
</div>
</div>
</div>
}
}

View file

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Artist}} - {{.Title}} lyrics</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/static/style.css" />
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
<script type="text/javascript" src="/static/script.js" defer></script>
</head>
<body>
{{template "navbar"}}
<div id="container">
<div id="metadata">
<a href="{{.LinkToAlbum}}"><img id="album-artwork" src="{{extractURL .Image}}"/></a>
<h2>{{.Artist}}</h2>
<h1>{{.Title}}</h1>
<a href="{{.LinkToAlbum}}"><h2>{{.Album}}</h2></a>
</div>
<div id="lyrics">{{.Lyrics}}</div>
<div id="info">
<div id="about">
<h1 id="title">About</h1>
<p class="hidden" id="full_about">{{index .About 0}}</p>
<p id="summary">{{index .About 1}}</p>
</div>
<div id="credits">
<h1 id="title">Credits</h1>
{{range $key, $val := .Credits}}
<details>
<summary>{{$key}}</summary>
<p>{{$val}}</p>
</details>
{{end}}
</div>
</div>
</div>
{{template "footer"}}
</body>
</html>

7
views/navbar.templ Normal file
View file

@ -0,0 +1,7 @@
package views
templ navbar() {
<nav>
<a href="/"><img src="/static/logo.svg"/></a>
</nav>
}

View file

@ -1,5 +0,0 @@
{{define "navbar"}}
<nav>
<a href="/"><img src="/static/logo.svg" /></a>
</nav>
{{end}}

29
views/search.templ Normal file
View file

@ -0,0 +1,29 @@
package views
import "github.com/rramiachraf/dumb/data"
templ SearchPage(r data.SearchResults) {
@layout("Search - dumb") {
<div id="search-page" class="main">
<form method="GET">
<input type="text" name="q" id="search-input" placeholder="Search..." value={ r.Query }/>
</form>
<div id="search-results">
for _, s := range r.Sections {
if s.Type == "song" {
<h1>Songs</h1>
for _, s := range s.Hits {
<a id="search-item" href={ templ.URL(s.Result.Path) }>
<img src={ data.ExtractImageURL(s.Result.Thumbnail) }/>
<div>
<span>{ s.Result.ArtistNames }</span>
<h2>{ s.Result.Title }</h2>
</div>
</a>
}
}
}
</div>
</div>
}
}

View file

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Search - dumb</title>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/static/style.css" />
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
<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>