From 840d23e931d4cb22474837680f0488dc6f3f04cf Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 8 Jun 2024 15:16:13 +0200 Subject: [PATCH] feat: add artist page --- data/album.go | 29 +++++++++++------ data/artist.go | 65 ++++++++++++++++++++++++++++++++++++++ data/lyrics.go | 13 +++----- handlers/artist.go | 70 +++++++++++++++++++++++++++++++++++++++++ handlers/artist_test.go | 50 +++++++++++++++++++++++++++++ handlers/cache.go | 2 +- handlers/handler.go | 1 + style/artist.css | 25 +++++++++++++++ style/lyrics.css | 1 - views/album.templ | 2 +- views/artist.templ | 27 ++++++++++++++++ 11 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 data/artist.go create mode 100644 handlers/artist.go create mode 100644 handlers/artist_test.go create mode 100644 style/artist.css create mode 100644 views/artist.templ diff --git a/data/album.go b/data/album.go index ba2fec6..95a5f79 100644 --- a/data/album.go +++ b/data/album.go @@ -7,10 +7,15 @@ import ( "github.com/PuerkitoBio/goquery" ) +type AlbumPreview struct { + Name string + Image string + URL string +} + type Album struct { - Artist string - Name string - Image string + AlbumPreview + Artist ArtistPreview About [2]string Tracks []Track @@ -24,11 +29,11 @@ type Track struct { 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"` + Id int `json:"id"` + Image string `json:"cover_art_thumbnail_url"` + Name string `json:"name"` + Description string `json:"description_preview"` + artistPreviewMetadata `json:"artist"` } AlbumAppearances []AlbumAppearances `json:"album_appearances"` } @@ -42,8 +47,9 @@ type AlbumAppearances struct { } } -type Artist struct { +type artistPreviewMetadata struct { Name string `json:"name"` + URL string `json:"url"` } func (a *Album) parseAlbumData(doc *goquery.Document) error { @@ -58,7 +64,10 @@ func (a *Album) parseAlbumData(doc *goquery.Document) error { } albumData := albumMetadataFromPage.Album - a.Artist = albumData.Artist.Name + a.Artist = ArtistPreview{ + Name: albumData.artistPreviewMetadata.Name, + URL: strings.Replace(albumData.artistPreviewMetadata.URL, "https://genius.com", "", -1), + } a.Name = albumData.Name a.Image = albumData.Image a.About[0] = albumData.Description diff --git a/data/artist.go b/data/artist.go new file mode 100644 index 0000000..b5829a9 --- /dev/null +++ b/data/artist.go @@ -0,0 +1,65 @@ +package data + +import ( + "encoding/json" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +type ArtistPreview struct { + Name string + URL string +} + +type Artist struct { + Name string + Description string + Albums []AlbumPreview + Image string +} + +type artistMetadata struct { + Artist struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description_preview"` + Image string `json:"image_url"` + } + Albums []struct { + Id int `json:"id"` + Image string `json:"cover_art_thumbnail_url"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"artist_albums"` +} + +func (a *Artist) parseArtistData(doc *goquery.Document) error { + pageMetadata, exists := doc.Find("meta[itemprop='page_data']").Attr("content") + if !exists { + return nil + } + + var artistMetadataFromPage artistMetadata + if err := json.Unmarshal([]byte(pageMetadata), &artistMetadataFromPage); err != nil { + return err + } + + a.Name = artistMetadataFromPage.Artist.Name + a.Description = artistMetadataFromPage.Artist.Description + a.Image = artistMetadataFromPage.Artist.Image + + for _, album := range artistMetadataFromPage.Albums { + a.Albums = append(a.Albums, AlbumPreview{ + Name: album.Name, + Image: album.Image, + URL: strings.Replace(album.URL, "https://genius.com", "", -1), + }) + } + + return nil +} + +func (a *Artist) Parse(doc *goquery.Document) error { + return a.parseArtistData(doc) +} diff --git a/data/lyrics.go b/data/lyrics.go index 016258e..60d378f 100644 --- a/data/lyrics.go +++ b/data/lyrics.go @@ -16,11 +16,7 @@ type Song struct { Lyrics string Credits map[string]string About [2]string - Album struct { - URL string - Name string - Image string - } + Album AlbumPreview } type songResponse struct { @@ -38,10 +34,10 @@ type songResponse struct { Image string `json:"cover_art_url"` } CustomPerformances []customPerformance `json:"custom_performances"` - WriterArtists []struct { + WriterArtists []struct { Name string } `json:"writer_artists"` - ProducerArtists []struct { + ProducerArtists []struct { Name string } `json:"producer_artists"` } @@ -117,7 +113,8 @@ func (s *Song) parseSongData(doc *goquery.Document) error { func joinNames(data []struct { Name string -}) string { +}, +) string { var names []string for _, hasName := range data { names = append(names, hasName.Name) diff --git a/handlers/artist.go b/handlers/artist.go new file mode 100644 index 0000000..4406133 --- /dev/null +++ b/handlers/artist.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "context" + "fmt" + "net/http" + + "github.com/PuerkitoBio/goquery" + "github.com/gorilla/mux" + "github.com/rramiachraf/dumb/data" + "github.com/rramiachraf/dumb/utils" + "github.com/rramiachraf/dumb/views" +) + +func artist(l *utils.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + artistName := mux.Vars(r)["artist"] + + id := fmt.Sprintf("artist:%s", artistName) + + if a, err := getCache[data.Artist](id); err == nil { + views.ArtistPage(a).Render(context.Background(), w) + return + } + + url := fmt.Sprintf("https://genius.com/artists/%s", artistName) + + resp, err := utils.SendRequest(url) + if err != nil { + l.Error(err.Error()) + 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.Error(err.Error()) + 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.Error("cloudflare got in the way") + views.ErrorPage(500, "cloudflare is detected").Render(context.Background(), w) + return + } + + var a data.Artist + if err = a.Parse(doc); err != nil { + l.Error(err.Error()) + } + + views.ArtistPage(a).Render(context.Background(), w) + + if err = setCache(id, a); err != nil { + l.Error(err.Error()) + } + } +} diff --git a/handlers/artist_test.go b/handlers/artist_test.go new file mode 100644 index 0000000..b51ca32 --- /dev/null +++ b/handlers/artist_test.go @@ -0,0 +1,50 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/PuerkitoBio/goquery" + + "github.com/rramiachraf/dumb/utils" +) + +func TestArtist(t *testing.T) { + url := "/artists/Red-hot-chili-peppers" + name := "Red Hot Chili Peppers" + firstAlbumName := "Cardiff, Wales: 6/23/04" + + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := utils.NewLogger(os.Stdout) + m := New(l, &assets{}) + + m.ServeHTTP(rr, r) + + defer rr.Result().Body.Close() + + if rr.Result().StatusCode != http.StatusOK { + t.Fatalf("expected %d, got %d\n", http.StatusOK, rr.Result().StatusCode) + } + + doc, err := goquery.NewDocumentFromReader(rr.Result().Body) + if err != nil { + t.Fatal(err) + } + + artistName := doc.Find("#metadata-info > h1").First().Text() + if artistName != name { + t.Fatalf("expected %q, got %q\n", name, artistName) + } + + albumName := doc.Find("#artist-albumlist > a > p").First().Text() + if albumName != firstAlbumName { + t.Fatalf("expected %q, got %q\n", firstAlbumName, albumName) + } +} diff --git a/handlers/cache.go b/handlers/cache.go index 4874057..d07a876 100644 --- a/handlers/cache.go +++ b/handlers/cache.go @@ -10,7 +10,7 @@ import ( ) type cachable interface { - data.Album | data.Song | data.Annotation | []byte + data.Album | data.Song | data.Annotation | data.Artist | []byte } var c, _ = bigcache.New(context.Background(), bigcache.DefaultConfig(time.Hour*24)) diff --git a/handlers/handler.go b/handlers/handler.go index 738bf7e..9cb32c7 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -22,6 +22,7 @@ func New(logger *utils.Logger, staticFiles static) *mux.Router { w.Write([]byte("User-agent: *\nDisallow: /\n")) }) r.HandleFunc("/albums/{artist}/{albumName}", album(logger)).Methods("GET") + r.HandleFunc("/artists/{artist}", artist(logger)).Methods("GET") r.HandleFunc("/images/{filename}.{ext}", imageProxy(logger)).Methods("GET") r.HandleFunc("/search", search(logger)).Methods("GET") r.HandleFunc("/{annotation-id}/{artist-song}/{verse}/annotations", annotations(logger)).Methods("GET") diff --git a/style/artist.css b/style/artist.css new file mode 100644 index 0000000..2144fd0 --- /dev/null +++ b/style/artist.css @@ -0,0 +1,25 @@ +artist-albumlist #artist-albumlist p { + color: #181d31; + font-weight: 500; +} + +.dark #artist-albumlist p { + color: #ddd; +} + +#artist-albumlist small { + font-size: 1.5rem; + color: #333; +} + +.dark #artist-albumlist small { + color: #ccc; +} + +#metadata p { + color: #171717; +} + +.dark #metadata p { + color: #ddd; +} diff --git a/style/lyrics.css b/style/lyrics.css index a527a86..972ed6a 100644 --- a/style/lyrics.css +++ b/style/lyrics.css @@ -132,4 +132,3 @@ text-align: center; } } - diff --git a/views/album.templ b/views/album.templ index bac1b96..32e002c 100644 --- a/views/album.templ +++ b/views/album.templ @@ -11,7 +11,7 @@ templ AlbumPage(a data.Album) {
Album image
-

{ a.Artist }

+

{ a.Artist.Name }

{ a.Name }

diff --git a/views/artist.templ b/views/artist.templ new file mode 100644 index 0000000..80903de --- /dev/null +++ b/views/artist.templ @@ -0,0 +1,27 @@ +package views + +import ( + "github.com/rramiachraf/dumb/data" +) + +templ ArtistPage(a data.Artist) { + @layout(a.Name) { +
+
+ Artist image +
+

{ a.Name }

+

@templ.Raw(a.Description)

+
+
+
+ for _, album := range a.Albums { + + Artist image +

{ album.Name }

+
+ } +
+
+ } +}