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)) { +
+
+ +

{ a.Artist }

+

{ a.Name }

+
+
+ for _, t := range a.Tracks { + +

{ t.Title }

+
+ } +
+
+
+

About

+ +

{ 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"}} -
-
- -

{{.Artist}}

-

{{.Name}}

-
-
- {{range .Tracks}} - -

{{.Title}}

-
- {{end}} -
-
-
-

About

- -

{{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)) { +
+
+ +

{ s.Artist }

+

{ s.Title }

+

{ s.Album }

+
+
+ @templ.Raw(s.Lyrics) +
+
+
+

About

+ +

{ 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"}} -
-
- -

{{.Artist}}

-

{{.Title}}

-

{{.Album}}

-
-
{{.Lyrics}}
-
-
-

About

- -

{{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") { +
+
+ +
+
+ for _, s := range r.Sections { + if s.Type == "song" { +

Songs

+ for _, s := range s.Hits { + + +
+ { s.Result.ArtistNames } +

{ s.Result.Title }

+
+
+ } + } + } +
+
+ } +} 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"}} -
-
- -
-
- {{range .Sections}} - {{if eq .Type "song"}} -

Songs

- {{range .Hits}} - - -
- {{.Result.ArtistNames}} -

{{.Result.Title}}

-
-
- {{end}} - {{end}} - {{end}} -
-
- {{template "footer"}} - - -