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+
3042type 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-
4248var 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
218244func 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+ }
0 commit comments