From 100d3e9d99ffadf4138ad8f3fe11110d2f70f10e Mon Sep 17 00:00:00 2001 From: Araozu Date: Tue, 8 Oct 2024 07:55:00 -0500 Subject: [PATCH] feat: Load albums and covers --- src/modules/auth/login.go | 8 +++ src/modules/covers/covers.go | 31 ++++++++++ src/modules/covers/covers.service.go | 31 ++++++++++ src/modules/index/index.go | 16 ++--- src/modules/index/index.service.go | 33 +++++++++++ src/modules/index/index.templ | 30 +++++++++- src/modules/index/index_templ.go | 87 +++++++++++++++++++++++++++- src/routes.go | 2 + src/utils/types.go | 46 +++++++++++++++ src/utils/utils.go | 40 +++++++++++++ 10 files changed, 310 insertions(+), 14 deletions(-) create mode 100644 src/modules/covers/covers.go create mode 100644 src/modules/covers/covers.service.go create mode 100644 src/modules/index/index.service.go diff --git a/src/modules/auth/login.go b/src/modules/auth/login.go index e1651e2..e653c9d 100644 --- a/src/modules/auth/login.go +++ b/src/modules/auth/login.go @@ -11,6 +11,12 @@ import ( // Renders the loginPage form func loginPage(c echo.Context) error { + // if the request has the required cookies, redirect to / + _, err := c.Cookie("session-token") + _, err2 := c.Cookie("navidrome-url") + if err == nil && err2 == nil { + return c.Redirect(http.StatusFound, "/") + } return utils.RenderTempl(c, http.StatusOK, LoginTempl()) } @@ -36,6 +42,7 @@ func loginFragment(c echo.Context) error { cookie1.Path = "/" cookie1.HttpOnly = true cookie1.Secure = true + cookie1.SameSite = http.SameSiteStrictMode c.SetCookie(cookie1) cookie2 := new(http.Cookie) @@ -45,6 +52,7 @@ func loginFragment(c echo.Context) error { cookie2.Path = "/" cookie2.HttpOnly = true cookie2.Secure = true + cookie2.SameSite = http.SameSiteStrictMode c.SetCookie(cookie2) return c.HTML(http.StatusOK, "
Logged in, redirecting...
") diff --git a/src/modules/covers/covers.go b/src/modules/covers/covers.go new file mode 100644 index 0000000..5ff5610 --- /dev/null +++ b/src/modules/covers/covers.go @@ -0,0 +1,31 @@ +package covers + +import ( + "acide/src/utils" + "log" + "net/http" + + "github.com/labstack/echo" +) + +func Setup(g *echo.Group) { + log.Print("Setting up the covers module") + + g.Use(utils.Authed) + + g.GET("/:id", getCover) +} + +func getCover(c echo.Context) error { + token, server := utils.Credentials(c) + albumId := c.Param("id") + + coverBytes, err := loadCover(token, server, albumId) + if err != nil { + return err + } + + c.Response().Header().Set("Cache-Control", "max-age=604800") + + return c.Blob(http.StatusOK, "image/png", coverBytes) +} diff --git a/src/modules/covers/covers.service.go b/src/modules/covers/covers.service.go new file mode 100644 index 0000000..3d28187 --- /dev/null +++ b/src/modules/covers/covers.service.go @@ -0,0 +1,31 @@ +package covers + +import ( + "errors" + "fmt" + + "github.com/go-resty/resty/v2" +) + +func loadCover(token, server, albumId string) ([]byte, error) { + + response, err := resty.New().R(). + SetHeader("x-nd-authorization", fmt.Sprintf("Bearer %s", token)). + Get(fmt.Sprintf( + "%s/rest/getCoverArt.view?id=%s&u=%s&s=12e7f3&t=%s&v=1.13.0&c=wmusic&size=300", + server, + albumId, + "fernando", + "d7bbe92d7da363aa202ae16136887adc", + )) + + if err != nil { + return nil, err + } + + if !response.IsSuccess() { + return nil, errors.New("Error fetching image from server") + } + + return response.Body(), nil +} diff --git a/src/modules/index/index.go b/src/modules/index/index.go index 2534e8f..918cf5f 100644 --- a/src/modules/index/index.go +++ b/src/modules/index/index.go @@ -2,28 +2,28 @@ package index import ( "acide/src/utils" + "fmt" "log" "net/http" "github.com/labstack/echo" ) -// Registers all the routes that this module (auth) provides func SetupRoutes(g *echo.Group) { log.Print("Setting up the index module") + g.Use(utils.Authed) + // To include custom rendering logic: g.GET("/", indexPage) } func indexPage(c echo.Context) error { - // If the required cookies are set, redirect to home - _, err1 := c.Cookie("session-token") - _, err2 := c.Cookie("navidrome-url") - - if err1 != nil || err2 != nil { - return c.Redirect(http.StatusFound, "/auth/") + sessionToken, navidromeUrl := utils.Credentials(c) + albums, err := getRandomAlbums(sessionToken, navidromeUrl, 10) + if err != nil { + return c.HTML(http.StatusBadRequest, fmt.Sprintf("%s", err)) } - return utils.RenderTempl(c, http.StatusOK, IndexTempl()) + return utils.RenderTempl(c, http.StatusOK, IndexTempl(albums)) } diff --git a/src/modules/index/index.service.go b/src/modules/index/index.service.go new file mode 100644 index 0000000..add34e7 --- /dev/null +++ b/src/modules/index/index.service.go @@ -0,0 +1,33 @@ +package index + +import ( + "acide/src/utils" + "errors" + "fmt" + + "github.com/go-resty/resty/v2" +) + +// Gets `amount` random albums from the server +func getRandomAlbums(token, server string, amount int) ([]utils.Album, error) { + var albums []utils.Album + var error utils.NavError + + client := resty.New() + + response, err := client.R(). + SetHeader("x-nd-authorization", fmt.Sprintf("Bearer %s", token)). + SetResult(&albums). + SetError(&error). + Get(fmt.Sprintf("%s/api/album?_end=%d&_order=DESC&_sort=random&_start=0", server, amount)) + + if err != nil { + return nil, err + } + + if !response.IsSuccess() { + return nil, errors.New(fmt.Sprintf("Error getting albums: %s", error.Error)) + } + + return albums, nil +} diff --git a/src/modules/index/index.templ b/src/modules/index/index.templ index bdad796..709fcf6 100644 --- a/src/modules/index/index.templ +++ b/src/modules/index/index.templ @@ -1,11 +1,35 @@ package index -import "acide/src/utils" +import ( + "acide/src/utils" + "fmt" +) -templ IndexTempl() { +templ IndexTempl(albums []utils.Album) { @utils.SkeletonTempl() {
- Home page :D + +
+ for _, album := range albums { + @albumCard(album) + } +
} } + +templ albumCard(album utils.Album) { +
+
+ +
+ { album.Name } +
+
+ { album.Artist } +
+
+
+} diff --git a/src/modules/index/index_templ.go b/src/modules/index/index_templ.go index 1536b36..fbf98e5 100644 --- a/src/modules/index/index_templ.go +++ b/src/modules/index/index_templ.go @@ -8,9 +8,12 @@ package index import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "acide/src/utils" +import ( + "acide/src/utils" + "fmt" +) -func IndexTempl() templ.Component { +func IndexTempl(albums []utils.Album) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -43,7 +46,17 @@ func IndexTempl() templ.Component { }() } ctx = templ.InitializeContext(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Home page :D
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, album := range albums { + templ_7745c5c3_Err = albumCard(album).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -57,4 +70,72 @@ func IndexTempl() templ.Component { }) } +func albumCard(album utils.Album) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(album.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `src/modules/index/index.templ`, Line: 28, Col: 16} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(album.Artist) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `src/modules/index/index.templ`, Line: 31, Col: 18} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + var _ = templruntime.GeneratedTemplate diff --git a/src/routes.go b/src/routes.go index 859dbbd..50b3bab 100644 --- a/src/routes.go +++ b/src/routes.go @@ -2,6 +2,7 @@ package src import ( "acide/src/modules/auth" + "acide/src/modules/covers" "acide/src/modules/index" "net/http" "os" @@ -27,6 +28,7 @@ func (s *Server) RegisterRoutes() http.Handler { // NOTE: Register subroutes here index.SetupRoutes(e.Group("")) auth.SetupRoutes(e.Group("/auth")) + covers.Setup(e.Group("/covers")) return e } diff --git a/src/utils/types.go b/src/utils/types.go index b0939a5..f0bafc8 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -1,5 +1,7 @@ package utils +import "time" + // The result of a Login type AuthSuccess struct { Id string `json:"id"` @@ -10,3 +12,47 @@ type AuthSuccess struct { Token string `json:"token"` Username string `json:"username"` } + +// An album as defined by the server +type Album struct { + PlayCount int `json:"playCount"` + PlayDate time.Time `json:"playDate"` + Rating int `json:"rating"` + Starred bool `json:"starred"` + StarredAt time.Time `json:"starredAt"` + ID string `json:"id"` + Name string `json:"name"` + EmbedArtPath string `json:"embedArtPath"` + ArtistID string `json:"artistId"` + Artist string `json:"artist"` + AlbumArtistID string `json:"albumArtistId"` + AlbumArtist string `json:"albumArtist"` + AllArtistIds string `json:"allArtistIds"` + MaxYear int `json:"maxYear"` + MinYear int `json:"minYear"` + Compilation bool `json:"compilation"` + SongCount int `json:"songCount"` + Duration float64 `json:"duration"` + Size int `json:"size"` + Genre string `json:"genre"` + Genres []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"genres"` + FullText string `json:"fullText"` + OrderAlbumName string `json:"orderAlbumName"` + OrderAlbumArtistName string `json:"orderAlbumArtistName"` + ImageFiles string `json:"imageFiles"` + Paths string `json:"paths"` + SmallImageURL string `json:"smallImageUrl"` + MediumImageURL string `json:"mediumImageUrl"` + LargeImageURL string `json:"largeImageUrl"` + ExternalURL string `json:"externalUrl"` + ExternalInfoUpdatedAt time.Time `json:"externalInfoUpdatedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type NavError struct { + Error string `json:"error"` +} diff --git a/src/utils/utils.go b/src/utils/utils.go index 3922fb0..b5ad2c2 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -4,11 +4,34 @@ import ( "bytes" "errors" "log" + "net/http" "github.com/a-h/templ" "github.com/labstack/echo" ) +// Middleware that allows only requests with the `session-token` and `navidrome-url` cookies set +func Authed(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + _, err := c.Cookie("session-token") + if err != nil { + c.Redirect(http.StatusFound, "/auth/") + return nil + } + _, err = c.Cookie("navidrome-url") + if err != nil { + c.Redirect(http.StatusFound, "/auth/") + return nil + } + + if err := next(c); err != nil { + c.Error(err) + } + + return nil + } +} + // Renders a template and sends it with a custom http status code func RenderTempl(c echo.Context, status int, cmp templ.Component) error { var buff bytes.Buffer @@ -19,3 +42,20 @@ func RenderTempl(c echo.Context, status int, cmp templ.Component) error { return c.HTML(status, buff.String()) } + +// Returns the `sessionToken` and `navidromeUrl` cookies. +// This function must be called by a route protected by the Auth +// middleware, otherwise it will panic +func Credentials(c echo.Context) (string, string) { + sessionToken, err := c.Cookie("session-token") + if err != nil { + panic("Error getting credentials from cookie: session-token was not set") + } + + navidromeUrl, err := c.Cookie("navidrome-url") + if err != nil { + panic("Error getting credentials from cookie: navidrome-url was not set") + } + + return sessionToken.Value, navidromeUrl.Value +}