Skip to content

Commit a508362

Browse files
committed
profile canvas wip
1 parent 0f9206b commit a508362

File tree

10 files changed

+255
-56
lines changed

10 files changed

+255
-56
lines changed

assets/font/Inter_24pt-Bold.ttf

336 KB
Binary file not shown.

assets/img/profile_layout.png

6.72 KB
Loading

bild/imgio/imgio.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,31 @@ func Fetch(url string) ([]byte, error) {
6464
return data, nil
6565
}
6666

67+
// FromUrl retrieves an image from the given URL and decodes it.
68+
func FromUrl(url string) (image.Image, error) {
69+
resp, err := http.Get(url)
70+
if err != nil {
71+
return nil, err
72+
}
73+
defer resp.Body.Close()
74+
75+
if resp.StatusCode != http.StatusOK {
76+
return nil, fmt.Errorf("failed to fetch image: %s", resp.Status)
77+
}
78+
79+
data, err := io.ReadAll(resp.Body)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
img, _, err := image.Decode(bytes.NewReader(data))
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
return img, nil
90+
}
91+
6792
// DecodeImage loads and decodes an image from a byte slice and returns it.
6893
//
6994
// Usage example:

commands/chart/chart.go

Lines changed: 115 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
"image"
88
"image/color"
99
"image/draw"
10-
"image/gif"
1110
"net/http"
11+
"sync"
12+
"time"
1213

1314
"github.com/nxtgo/arikawa/v3/api"
1415
"github.com/nxtgo/arikawa/v3/discord"
@@ -25,20 +26,25 @@ var (
2526
maxGridSize = 10
2627
minGridSize = 3
2728
defaultPeriod = "overall"
29+
brokenImage image.Image
30+
maxConcurrent = 8
2831
)
2932

33+
var httpClient = &http.Client{
34+
Transport: &http.Transport{
35+
MaxIdleConns: 100,
36+
MaxIdleConnsPerHost: 10,
37+
IdleConnTimeout: 90 * time.Second,
38+
},
39+
Timeout: 10 * time.Second,
40+
}
41+
3042
type Entry struct {
3143
Image image.Image
3244
Name string
3345
Artist string
3446
}
3547

36-
type deezerSearchResponse struct {
37-
Data []struct {
38-
Picture string `json:"picture"`
39-
} `json:"data"`
40-
}
41-
4248
var data = api.CreateCommandData{
4349
Name: "chart",
4450
Description: "Your top artists/tracks/albums but with images",
@@ -99,29 +105,7 @@ func handler(c *commands.CommandContext) error {
99105
return err
100106
}
101107

102-
brokenImage, _ := imgio.Open("assets/img/broken.png")
103-
brokenImage = transform.Resize(brokenImage, 300, 300, transform.Gaussian)
104-
105-
fetchImage := func(url string) image.Image {
106-
resp, err := http.Get(url)
107-
if err != nil {
108-
return brokenImage
109-
}
110-
defer resp.Body.Close()
111-
112-
img, _, err := image.Decode(resp.Body)
113-
if err == nil {
114-
return img
115-
}
116-
117-
gifImg, err := gif.Decode(resp.Body)
118-
if err != nil {
119-
return brokenImage
120-
}
121-
return gifImg
122-
}
123-
124-
var entries []Entry
108+
entries := make([]Entry, 0, gridSize*gridSize)
125109

126110
switch options.Type {
127111
case "artist":
@@ -130,51 +114,89 @@ func handler(c *commands.CommandContext) error {
130114
return err
131115
}
132116

133-
for _, a := range topArtists.Artists {
117+
urls := make([]string, len(topArtists.Artists))
118+
names := make([]string, len(topArtists.Artists))
119+
for i, a := range topArtists.Artists {
134120
imgURL, err := a.GetDeezerImage()
135121
if err != nil || imgURL == "" {
136-
entries = append(entries, Entry{Image: brokenImage, Name: a.Name})
137-
continue
122+
urls[i] = ""
123+
} else {
124+
urls[i] = imgURL
125+
}
126+
names[i] = a.Name
127+
}
128+
fetched := fetchEntries(urls)
129+
for i, e := range fetched {
130+
if e.Image == nil {
131+
e.Image = brokenImage
138132
}
139-
img := fetchImage(imgURL)
140-
img = transform.Resize(img, 300, 300, transform.Gaussian)
141-
entries = append(entries, Entry{Image: img, Name: a.Name})
133+
e.Image = transform.Resize(e.Image, 300, 300, transform.NearestNeighbor)
134+
e.Name = names[i]
135+
entries = append(entries, e)
142136
}
143137

144138
case "track":
145139
topTracks, err := c.Last.User.GetTopTracks(lastfm.P{"user": username, "limit": gridSize * gridSize, "period": period})
146140
if err != nil {
147141
return err
148142
}
149-
for _, t := range topTracks.Tracks {
150-
img := brokenImage
143+
144+
urls := make([]string, len(topTracks.Tracks))
145+
names := make([]string, len(topTracks.Tracks))
146+
artists := make([]string, len(topTracks.Tracks))
147+
for i, t := range topTracks.Tracks {
151148
if len(t.Images) > 0 {
152-
img = fetchImage(t.Images[len(t.Images)-1].URL)
149+
urls[i] = t.Images[len(t.Images)-1].URL
150+
}
151+
names[i] = t.Name
152+
artists[i] = t.Artist.Name
153+
}
154+
fetched := fetchEntries(urls)
155+
for i, e := range fetched {
156+
if e.Image == nil {
157+
e.Image = brokenImage
153158
}
154-
entries = append(entries, Entry{Image: img, Name: t.Name, Artist: t.Artist.Name})
159+
e.Name = names[i]
160+
e.Artist = artists[i]
161+
entries = append(entries, e)
155162
}
156163

157164
case "album":
158165
topAlbums, err := c.Last.User.GetTopAlbums(lastfm.P{"user": username, "limit": gridSize * gridSize, "period": period})
159166
if err != nil {
160167
return err
161168
}
162-
for _, a := range topAlbums.Albums {
163-
img := brokenImage
169+
170+
urls := make([]string, len(topAlbums.Albums))
171+
names := make([]string, len(topAlbums.Albums))
172+
artists := make([]string, len(topAlbums.Albums))
173+
for i, a := range topAlbums.Albums {
164174
if len(a.Images) > 0 {
165-
img = fetchImage(a.Images[len(a.Images)-1].URL)
175+
urls[i] = a.Images[len(a.Images)-1].URL
166176
}
167-
entries = append(entries, Entry{Image: img, Name: a.Name, Artist: a.Artist.Name})
177+
names[i] = a.Name
178+
artists[i] = a.Artist.Name
179+
}
180+
fetched := fetchEntries(urls)
181+
for i, e := range fetched {
182+
if e.Image == nil {
183+
e.Image = brokenImage
184+
}
185+
e.Name = names[i]
186+
e.Artist = artists[i]
187+
entries = append(entries, e)
168188
}
169189
}
170190

171191
if len(entries) == 0 {
172192
return errors.New("no entries found")
173193
}
174194

175-
inter := font.LoadFont("assets/font/Inter_24pt-Regular.ttf")
176-
labelFace := inter.Face(20, 72)
177-
subFace := inter.Face(16, 72)
195+
interRegular := font.LoadFont("assets/font/Inter_24pt-Regular.ttf")
196+
interBold := font.LoadFont("assets/font/Inter_24pt-Bold.ttf")
197+
198+
labelFace := interBold.Face(20, 72)
199+
subFace := interRegular.Face(16, 72)
178200

179201
firstBounds := entries[0].Image.Bounds()
180202
cellWidth := firstBounds.Dx()
@@ -188,6 +210,9 @@ func handler(c *commands.CommandContext) error {
188210
return err
189211
}
190212

213+
labelAscent := labelFace.Metrics().Ascent.Ceil()
214+
subAscent := subFace.Metrics().Ascent.Ceil()
215+
191216
for i, entry := range entries {
192217
row := i / gridSize
193218
col := i % gridSize
@@ -197,10 +222,11 @@ func handler(c *commands.CommandContext) error {
197222

198223
draw.Draw(canvas, rect, entry.Image, image.Point{}, draw.Over)
199224
draw.Draw(canvas, rect, chartGradient, image.Point{}, draw.Over)
200-
font.DrawText(canvas, x+15, y+labelFace.Metrics().Ascent.Ceil()+15, entry.Name, color.White, labelFace)
225+
226+
font.DrawText(canvas, x+15, y+labelAscent+15, entry.Name, color.White, labelFace)
201227

202228
if entry.Artist != "" {
203-
font.DrawText(canvas, x+15, y+labelFace.Metrics().Ascent.Ceil()+subFace.Metrics().Ascent.Ceil()+25,
229+
font.DrawText(canvas, x+15, y+labelAscent+subAscent+20,
204230
entry.Artist, color.RGBA{170, 170, 170, 255}, subFace)
205231
}
206232
}
@@ -210,11 +236,50 @@ func handler(c *commands.CommandContext) error {
210236
return err
211237
}
212238

213-
_, err = edit.File(sendpart.File{Name: "chart.png", Reader: bytes.NewReader(result)}).Send()
239+
_, err = edit.Contentf("%s %s chart for %s", period, options.Type, username).File(sendpart.File{Name: "chart.png", Reader: bytes.NewReader(result)}).Send()
214240
return err
215241
})
216242
}
217243

218244
func init() {
245+
// todo: remove this in the future*
246+
brokenImage, _ = imgio.Open("assets/img/broken.png")
247+
brokenImage = transform.Resize(brokenImage, 300, 300, transform.NearestNeighbor)
219248
commands.Register(data, handler)
220249
}
250+
251+
func fetchImage(url string) image.Image {
252+
if url == "" {
253+
return nil
254+
}
255+
resp, err := httpClient.Get(url)
256+
if err != nil {
257+
return nil
258+
}
259+
defer resp.Body.Close()
260+
261+
img, _, err := image.Decode(resp.Body)
262+
if err != nil {
263+
return nil
264+
}
265+
return img
266+
}
267+
268+
func fetchEntries(urls []string) []Entry {
269+
entries := make([]Entry, len(urls))
270+
var wg sync.WaitGroup
271+
sem := make(chan struct{}, maxConcurrent)
272+
273+
for i, url := range urls {
274+
i, url := i, url
275+
wg.Go(func() {
276+
sem <- struct{}{}
277+
defer func() { <-sem }()
278+
279+
entries[i].Image = fetchImage(url)
280+
})
281+
}
282+
283+
wg.Wait()
284+
return entries
285+
}

commands/commands.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,8 @@ func RegisterCommands(r *cmdroute.Router, st *state.State, q *db.Queries, c *las
5353
start := time.Now()
5454
err := h(commandContext)
5555
zlog.Log.Debugw("executed command %s", zlog.F{"time": time.Since(start)}, name)
56-
// debugging purposes
56+
5757
if err != nil {
58-
zlog.Log.Warn(err.Error())
5958
commandContext.Reply.QuickEmbed(reply.ErrorEmbed(err.Error()))
6059
}
6160

commands/fm/fm.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/nxtgo/arikawa/v3/api"
77
"github.com/nxtgo/arikawa/v3/discord"
8+
"go.fm/bild/colors"
89
"go.fm/commands"
910
lastfm "go.fm/last.fm"
1011
"go.fm/pkg/components"
@@ -53,12 +54,18 @@ func handler(c *commands.CommandContext) error {
5354
text = components.NewTextDisplayf("-# *Last track for %s, scrobbled at %s*", res.User, playtime.Format(time.Kitchen))
5455
}
5556

56-
container := components.NewContainer(703487,
57+
thumbnail := lastTrack.GetLargestImage().URL
58+
color := 0x703487
59+
if dominantColor, err := colors.Dominant(thumbnail); err == nil {
60+
color = dominantColor
61+
}
62+
63+
container := components.NewContainer(color,
5764
components.NewSection(
5865
components.NewTextDisplayf("# %s", lastTrack.Name),
5966
components.NewTextDisplayf("**%s** **·** %s", lastTrack.Artist.Name, lastTrack.Album.Name),
6067
text,
61-
).WithAccessory(components.NewThumbnail(lastTrack.GetLargestImage().URL)),
68+
).WithAccessory(components.NewThumbnail(thumbnail)),
6269
components.NewActionRow(
6370
components.NewButton(components.ButtonStyleLink, "Last.fm", nil).WithEmoji("1418269025959546943").WithURL(lastTrack.URL),
6471
),

0 commit comments

Comments
 (0)