feat: Load albums and covers

This commit is contained in:
Araozu 2024-10-08 07:55:00 -05:00
parent abba2bfd03
commit 100d3e9d99
10 changed files with 310 additions and 14 deletions

View File

@ -11,6 +11,12 @@ import (
// Renders the loginPage form // Renders the loginPage form
func loginPage(c echo.Context) error { 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()) return utils.RenderTempl(c, http.StatusOK, LoginTempl())
} }
@ -36,6 +42,7 @@ func loginFragment(c echo.Context) error {
cookie1.Path = "/" cookie1.Path = "/"
cookie1.HttpOnly = true cookie1.HttpOnly = true
cookie1.Secure = true cookie1.Secure = true
cookie1.SameSite = http.SameSiteStrictMode
c.SetCookie(cookie1) c.SetCookie(cookie1)
cookie2 := new(http.Cookie) cookie2 := new(http.Cookie)
@ -45,6 +52,7 @@ func loginFragment(c echo.Context) error {
cookie2.Path = "/" cookie2.Path = "/"
cookie2.HttpOnly = true cookie2.HttpOnly = true
cookie2.Secure = true cookie2.Secure = true
cookie2.SameSite = http.SameSiteStrictMode
c.SetCookie(cookie2) c.SetCookie(cookie2)
return c.HTML(http.StatusOK, "<div _=\"init js window.location.href = '/'\">Logged in, redirecting...</div>") return c.HTML(http.StatusOK, "<div _=\"init js window.location.href = '/'\">Logged in, redirecting...</div>")

View File

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

View File

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

View File

@ -2,28 +2,28 @@ package index
import ( import (
"acide/src/utils" "acide/src/utils"
"fmt"
"log" "log"
"net/http" "net/http"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
// Registers all the routes that this module (auth) provides
func SetupRoutes(g *echo.Group) { func SetupRoutes(g *echo.Group) {
log.Print("Setting up the index module") log.Print("Setting up the index module")
g.Use(utils.Authed)
// To include custom rendering logic: // To include custom rendering logic:
g.GET("/", indexPage) g.GET("/", indexPage)
} }
func indexPage(c echo.Context) error { func indexPage(c echo.Context) error {
// If the required cookies are set, redirect to home sessionToken, navidromeUrl := utils.Credentials(c)
_, err1 := c.Cookie("session-token") albums, err := getRandomAlbums(sessionToken, navidromeUrl, 10)
_, err2 := c.Cookie("navidrome-url") if err != nil {
return c.HTML(http.StatusBadRequest, fmt.Sprintf("%s", err))
if err1 != nil || err2 != nil {
return c.Redirect(http.StatusFound, "/auth/")
} }
return utils.RenderTempl(c, http.StatusOK, IndexTempl()) return utils.RenderTempl(c, http.StatusOK, IndexTempl(albums))
} }

View File

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

View File

@ -1,11 +1,35 @@
package index package index
import "acide/src/utils" import (
"acide/src/utils"
"fmt"
)
templ IndexTempl() { templ IndexTempl(albums []utils.Album) {
@utils.SkeletonTempl() { @utils.SkeletonTempl() {
<div> <div>
Home page :D <nav class="text-blue-500 font-bold text-xl py-2 border-b border-blue-500">
music to go
</nav>
<div class="overflow-x-scroll whitespace-nowrap py-2">
for _, album := range albums {
@albumCard(album)
}
</div>
</div> </div>
} }
} }
templ albumCard(album utils.Album) {
<div class="inline-block p-1 mx-1 rounded bg-zinc-200 w-32">
<div class="h-30 relative">
<img src={ fmt.Sprintf("/covers/%s", album.ID) }/>
<div class="overflow-hidden overflow-ellipsis">
{ album.Name }
</div>
<div class="overflow-hidden overflow-ellipsis text-sm">
{ album.Artist }
</div>
</div>
</div>
}

View File

@ -8,9 +8,12 @@ package index
import "github.com/a-h/templ" import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime" 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) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@ -43,7 +46,17 @@ func IndexTempl() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div>Home page :D</div>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><nav class=\"text-blue-500 font-bold text-xl py-2 border-b border-blue-500\">music to go</nav><div class=\"overflow-x-scroll whitespace-nowrap py-2\">")
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("</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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("<div class=\"inline-block p-1 mx-1 rounded bg-zinc-200 w-32\"><div class=\"h-30 relative\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/covers/%s", album.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `src/modules/index/index.templ`, Line: 26, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"overflow-hidden overflow-ellipsis\">")
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("</div><div class=\"overflow-hidden overflow-ellipsis text-sm\">")
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("</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate var _ = templruntime.GeneratedTemplate

View File

@ -2,6 +2,7 @@ package src
import ( import (
"acide/src/modules/auth" "acide/src/modules/auth"
"acide/src/modules/covers"
"acide/src/modules/index" "acide/src/modules/index"
"net/http" "net/http"
"os" "os"
@ -27,6 +28,7 @@ func (s *Server) RegisterRoutes() http.Handler {
// NOTE: Register subroutes here // NOTE: Register subroutes here
index.SetupRoutes(e.Group("")) index.SetupRoutes(e.Group(""))
auth.SetupRoutes(e.Group("/auth")) auth.SetupRoutes(e.Group("/auth"))
covers.Setup(e.Group("/covers"))
return e return e
} }

View File

@ -1,5 +1,7 @@
package utils package utils
import "time"
// The result of a Login // The result of a Login
type AuthSuccess struct { type AuthSuccess struct {
Id string `json:"id"` Id string `json:"id"`
@ -10,3 +12,47 @@ type AuthSuccess struct {
Token string `json:"token"` Token string `json:"token"`
Username string `json:"username"` 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"`
}

View File

@ -4,11 +4,34 @@ import (
"bytes" "bytes"
"errors" "errors"
"log" "log"
"net/http"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/labstack/echo" "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 // Renders a template and sends it with a custom http status code
func RenderTempl(c echo.Context, status int, cmp templ.Component) error { func RenderTempl(c echo.Context, status int, cmp templ.Component) error {
var buff bytes.Buffer 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()) 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
}