mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
wip
This commit is contained in:
parent
c0a8c64243
commit
dfe3b5c90d
8 changed files with 317 additions and 2 deletions
BIN
assets/Jost-Regular.ttf
Normal file
BIN
assets/Jost-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/LeagueSpartan-Medium.ttf
Normal file
BIN
assets/LeagueSpartan-Medium.ttf
Normal file
Binary file not shown.
5
go.mod
5
go.mod
|
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/pressly/goose/v3 v3.24.3
|
github.com/pressly/goose/v3 v3.24.3
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/sync v0.14.0
|
golang.org/x/sync v0.18.0
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -60,7 +60,8 @@ require (
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/image v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -136,6 +136,8 @@ golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
|
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||||
|
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
|
@ -147,6 +149,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|
@ -161,6 +165,8 @@ 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
|
||||||
221
internal/summary/image.go
Normal file
221
internal/summary/image.go
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
package summary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
_ "image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gabehf/koito/internal/cfg"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/opentype"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Summary struct {
|
||||||
|
Title string
|
||||||
|
TopArtistImage string
|
||||||
|
TopArtists []struct {
|
||||||
|
Name string
|
||||||
|
Plays int
|
||||||
|
MinutesListened int
|
||||||
|
}
|
||||||
|
TopAlbumImage string
|
||||||
|
TopAlbums []struct {
|
||||||
|
Title string
|
||||||
|
Plays int
|
||||||
|
MinutesListened int
|
||||||
|
}
|
||||||
|
TopTrackImage string
|
||||||
|
TopTracks []struct {
|
||||||
|
Title string
|
||||||
|
Plays int
|
||||||
|
MinutesListened int
|
||||||
|
}
|
||||||
|
MinutesListened int
|
||||||
|
Plays int
|
||||||
|
AvgPlaysPerDay float32
|
||||||
|
UniqueTracks int32
|
||||||
|
UniqueAlbums int32
|
||||||
|
UniqueArtists int32
|
||||||
|
NewTracks int32
|
||||||
|
NewAlbums int32
|
||||||
|
NewArtists int32
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
assetPath = path.Join("..", "..", "assets")
|
||||||
|
titleFontPath = path.Join(assetPath, "LeagueSpartan-Medium.ttf")
|
||||||
|
textFontPath = path.Join(assetPath, "Jost-Regular.ttf")
|
||||||
|
paddingLg = 30
|
||||||
|
paddingMd = 20
|
||||||
|
paddingSm = 6
|
||||||
|
featuredImageSize = 180
|
||||||
|
titleFontSize = 48.0
|
||||||
|
textFontSize = 16.0
|
||||||
|
featureTextStart = paddingLg + paddingMd + featuredImageSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// lots of code borrowed from https://medium.com/@daniel.ruizcamacho/how-to-create-an-image-in-golang-step-by-step-4416affe088f
|
||||||
|
func GenerateImage(summary *Summary) error {
|
||||||
|
base := image.NewRGBA(image.Rect(0, 0, 750, 1100))
|
||||||
|
draw.Draw(base, base.Bounds(), image.NewUniform(color.Black), image.Pt(0, 0), draw.Over)
|
||||||
|
|
||||||
|
file, err := os.Create(path.Join(cfg.ConfigDir(), "summary.png"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// add title
|
||||||
|
if err := addText(base, summary.Title, "", image.Pt(paddingLg, 60), titleFontPath, titleFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
// add images
|
||||||
|
if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120), featuredImageSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)), featuredImageSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)*2), featuredImageSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
// top artists text
|
||||||
|
if err := addText(base, "Top Artists", "", image.Pt(featureTextStart, 132), textFontPath, textFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
for rank, artist := range summary.TopArtists {
|
||||||
|
if rank == 0 {
|
||||||
|
if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// top albums text
|
||||||
|
if err := addText(base, "Top Albums", "", image.Pt(featureTextStart, 132+featuredImageSize+paddingLg), textFontPath, textFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
for rank, album := range summary.TopAlbums {
|
||||||
|
if rank == 0 {
|
||||||
|
if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// top tracks text
|
||||||
|
|
||||||
|
// stats text
|
||||||
|
|
||||||
|
if err := png.Encode(file, base); err != nil {
|
||||||
|
return fmt.Errorf("GenerateImage: png.Encode: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addImage(baseImage *image.RGBA, path string, point image.Point, height int) error {
|
||||||
|
templateFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
template, _, err := image.Decode(templateFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resized := resize(template, height, height)
|
||||||
|
|
||||||
|
draw.Draw(baseImage, baseImage.Bounds(), resized, point, draw.Over)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addText(baseImage *image.RGBA, text, subtext string, point image.Point, fontFile string, fontSize float64) error {
|
||||||
|
fontBytes, err := os.ReadFile(fontFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ttf, err := opentype.Parse(fontBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
face, err := opentype.NewFace(ttf, &opentype.FaceOptions{
|
||||||
|
Size: fontSize,
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingFull,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer := &font.Drawer{
|
||||||
|
Dst: baseImage,
|
||||||
|
Src: image.NewUniform(color.White),
|
||||||
|
Face: face,
|
||||||
|
Dot: fixed.Point26_6{
|
||||||
|
X: fixed.I(point.X),
|
||||||
|
Y: fixed.I(point.Y),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer.DrawString(text)
|
||||||
|
if subtext != "" {
|
||||||
|
face, err = opentype.NewFace(ttf, &opentype.FaceOptions{
|
||||||
|
Size: textFontSize,
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingFull,
|
||||||
|
})
|
||||||
|
drawer.Face = face
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
drawer.Src = image.NewUniform(color.RGBA{200, 200, 200, 255})
|
||||||
|
drawer.DrawString(" - ")
|
||||||
|
drawer.DrawString(subtext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(m image.Image, w, h int) *image.RGBA {
|
||||||
|
if w < 0 || h < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := m.Bounds()
|
||||||
|
if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 {
|
||||||
|
return image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
}
|
||||||
|
curw, curh := r.Dx(), r.Dy()
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
for y := range h {
|
||||||
|
for x := range w {
|
||||||
|
// Get a source pixel.
|
||||||
|
subx := x * curw / w
|
||||||
|
suby := y * curh / h
|
||||||
|
r32, g32, b32, a32 := m.At(subx, suby).RGBA()
|
||||||
|
r := uint8(r32 >> 8)
|
||||||
|
g := uint8(g32 >> 8)
|
||||||
|
b := uint8(b32 >> 8)
|
||||||
|
a := uint8(a32 >> 8)
|
||||||
|
img.SetRGBA(x, y, color.RGBA{r, g, b, a})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
BIN
internal/summary/summary.png
Normal file
BIN
internal/summary/summary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
87
internal/summary/summary_test.go
Normal file
87
internal/summary/summary_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package summary_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gabehf/koito/internal/cfg"
|
||||||
|
"github.com/gabehf/koito/internal/summary"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(t *testing.M) {
|
||||||
|
// dir, err := utils.GenerateRandomString(8)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
cfg.Load(func(env string) string {
|
||||||
|
switch env {
|
||||||
|
case cfg.ENABLE_STRUCTURED_LOGGING_ENV:
|
||||||
|
return "true"
|
||||||
|
case cfg.LOG_LEVEL_ENV:
|
||||||
|
return "debug"
|
||||||
|
case cfg.DATABASE_URL_ENV:
|
||||||
|
return "postgres://postgres:secret@localhost"
|
||||||
|
case cfg.CONFIG_DIR_ENV:
|
||||||
|
return "."
|
||||||
|
case cfg.DISABLE_DEEZER_ENV, cfg.DISABLE_COVER_ART_ARCHIVE_ENV, cfg.DISABLE_MUSICBRAINZ_ENV, cfg.ENABLE_FULL_IMAGE_CACHE_ENV:
|
||||||
|
return "true"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}, "test")
|
||||||
|
t.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateImage(t *testing.T) {
|
||||||
|
s := summary.Summary{
|
||||||
|
Title: "20XX Rewind",
|
||||||
|
TopArtistImage: path.Join("..", "..", "test_assets", "yuu.jpg"),
|
||||||
|
TopArtists: []struct {
|
||||||
|
Name string
|
||||||
|
Plays int
|
||||||
|
MinutesListened int
|
||||||
|
}{
|
||||||
|
{"CHUU", 738, 7321},
|
||||||
|
{"Paramore", 738, 7321},
|
||||||
|
{"ano", 738, 7321},
|
||||||
|
{"NELKE", 738, 7321},
|
||||||
|
{"ILLIT", 738, 7321},
|
||||||
|
},
|
||||||
|
TopAlbumImage: "",
|
||||||
|
TopAlbums: []struct {
|
||||||
|
Title string
|
||||||
|
Plays int
|
||||||
|
MinutesListened int
|
||||||
|
}{
|
||||||
|
{"Only cry in the rain", 738, 7321},
|
||||||
|
{"Paramore", 738, 7321},
|
||||||
|
{"ano", 738, 7321},
|
||||||
|
{"NELKE", 738, 7321},
|
||||||
|
{"ILLIT", 738, 7321},
|
||||||
|
},
|
||||||
|
TopTrackImage: "",
|
||||||
|
TopTracks: []struct {
|
||||||
|
Title string
|
||||||
|
Plays int
|
||||||
|
MinutesListened int
|
||||||
|
}{
|
||||||
|
{"虹の色よ鮮やかであれ (NELKE ver.)", 321, 12351},
|
||||||
|
{"Paramore", 738, 7321},
|
||||||
|
{"ano", 738, 7321},
|
||||||
|
{"NELKE", 738, 7321},
|
||||||
|
{"ILLIT", 738, 7321},
|
||||||
|
},
|
||||||
|
MinutesListened: 0,
|
||||||
|
Plays: 0,
|
||||||
|
AvgPlaysPerDay: 0,
|
||||||
|
UniqueTracks: 0,
|
||||||
|
UniqueAlbums: 0,
|
||||||
|
UniqueArtists: 0,
|
||||||
|
NewTracks: 0,
|
||||||
|
NewAlbums: 0,
|
||||||
|
NewArtists: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, summary.GenerateImage(&s))
|
||||||
|
}
|
||||||
BIN
test_assets/default_img.webp
Normal file
BIN
test_assets/default_img.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Loading…
Add table
Add a link
Reference in a new issue