home to your local SPACEGIRL 馃挮
arimelody.space
1package api
2
3import (
4 "encoding/json"
5 "fmt"
6 "io/fs"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strings"
11 "time"
12
13 "arimelody-web/controller"
14 "arimelody-web/log"
15 "arimelody-web/model"
16)
17
18func ServeRelease(app *model.AppState, release *model.Release) http.Handler {
19 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20 // only allow authorised users to view hidden releases
21 privileged := false
22 if !release.Visible {
23 session, err := controller.GetSessionFromRequest(app, r)
24 if err != nil {
25 fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
26 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
27 return
28 }
29
30 if session != nil && session.Account != nil {
31 // TODO: check privilege on release
32 privileged = true
33 }
34
35 if !privileged {
36 http.NotFound(w, r)
37 return
38 }
39 }
40
41 type (
42 Track struct {
43 Title string `json:"title"`
44 Description string `json:"description"`
45 Lyrics string `json:"lyrics"`
46 }
47
48 Credit struct {
49 *model.Artist
50 Role string `json:"role"`
51 Primary bool `json:"primary"`
52 }
53
54 Release struct {
55 *model.Release
56 Tracks []Track `json:"tracks"`
57 Credits []Credit `json:"credits"`
58 Links map[string]string `json:"links"`
59 }
60 )
61
62 response := Release{
63 Release: release,
64 Tracks: []Track{},
65 Credits: []Credit{},
66 Links: make(map[string]string),
67 }
68
69 if release.IsReleased() || privileged {
70 // get credits
71 credits, err := controller.GetReleaseCredits(app.DB, release.ID)
72 if err != nil {
73 fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
74 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
75 return
76 }
77 for _, credit := range credits {
78 artist, err := controller.GetArtist(app.DB, credit.Artist.ID)
79 if err != nil {
80 fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
81 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
82 return
83 }
84
85 response.Credits = append(response.Credits, Credit{
86 Artist: artist,
87 Role: credit.Role,
88 Primary: credit.Primary,
89 })
90 }
91
92 // get tracks
93 tracks, err := controller.GetReleaseTracks(app.DB, release.ID)
94 if err != nil {
95 fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
96 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
97 return
98 }
99 for _, track := range tracks {
100 response.Tracks = append(response.Tracks, Track{
101 Title: track.Title,
102 Description: track.Description,
103 Lyrics: track.Lyrics,
104 })
105 }
106
107 // get links
108 links, err := controller.GetReleaseLinks(app.DB, release.ID)
109 if err != nil {
110 fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
111 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
112 return
113 }
114 for _, link := range links {
115 response.Links[link.Name] = link.URL
116 }
117 }
118
119 w.Header().Add("Content-Type", "application/json")
120 encoder := json.NewEncoder(w)
121 encoder.SetIndent("", "\t")
122 err := encoder.Encode(response)
123 if err != nil {
124 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
125 return
126 }
127 })
128}
129
130func ServeCatalog(app *model.AppState) http.Handler {
131 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132 releases, err := controller.GetAllReleases(app.DB, false, 0, true)
133 if err != nil {
134 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
135 return
136 }
137
138 type Release struct {
139 ID string `json:"id"`
140 Title string `json:"title"`
141 Artists []string `json:"artists"`
142 ReleaseType model.ReleaseType `json:"type" db:"type"`
143 ReleaseDate time.Time `json:"releaseDate" db:"release_date"`
144 Artwork string `json:"artwork"`
145 Buylink string `json:"buylink"`
146 Copyright string `json:"copyright" db:"copyright"`
147 }
148
149 catalog := []Release{}
150 session := r.Context().Value("session").(*model.Session)
151 for _, release := range releases {
152 if !release.Visible {
153 privileged := false
154 if session != nil && session.Account != nil {
155 // TODO: check privilege on release
156 privileged = true
157 }
158 if !privileged {
159 continue
160 }
161 }
162
163 artists := []string{}
164 for _, credit := range release.Credits {
165 if !credit.Primary { continue }
166 artists = append(artists, credit.Artist.Name)
167 }
168 catalog = append(catalog, Release{
169 ID: release.ID,
170 Title: release.Title,
171 Artists: artists,
172 ReleaseType: release.ReleaseType,
173 ReleaseDate: release.ReleaseDate,
174 Artwork: release.Artwork,
175 Buylink: release.Buylink,
176 Copyright: release.Copyright,
177 })
178 }
179
180 w.Header().Add("Content-Type", "application/json")
181 encoder := json.NewEncoder(w)
182 encoder.SetIndent("", "\t")
183 err = encoder.Encode(catalog)
184 if err != nil {
185 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
186 return
187 }
188 })
189}
190
191func CreateRelease(app *model.AppState) http.Handler {
192 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
193 session := r.Context().Value("session").(*model.Session)
194
195 var release model.Release
196 err := json.NewDecoder(r.Body).Decode(&release)
197 if err != nil {
198 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
199 return
200 }
201
202 if release.ID == "" {
203 http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
204 return
205 }
206
207 if release.Title == "" { release.Title = release.ID }
208 if release.ReleaseType == "" { release.ReleaseType = model.Single }
209
210 if release.ReleaseDate != time.Unix(0, 0) {
211 release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
212 }
213
214 if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" }
215
216 err = controller.CreateRelease(app.DB, &release)
217 if err != nil {
218 if strings.Contains(err.Error(), "duplicate key") {
219 http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
220 return
221 }
222 fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err)
223 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
224 return
225 }
226
227 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" created by \"%s\".", release.ID, session.Account.Username)
228
229 w.Header().Add("Content-Type", "application/json")
230 w.WriteHeader(http.StatusCreated)
231 encoder := json.NewEncoder(w)
232 encoder.SetIndent("", "\t")
233 err = encoder.Encode(release)
234 if err != nil {
235 fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err)
236 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
237 }
238 })
239}
240
241func UpdateRelease(app *model.AppState, release *model.Release) http.Handler {
242 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
243 session := r.Context().Value("session").(*model.Session)
244
245 if r.URL.Path == "/" {
246 http.NotFound(w, r)
247 return
248 }
249
250 segments := strings.Split(r.URL.Path[1:], "/")
251
252 if len(segments) == 2 {
253 switch segments[1] {
254 case "tracks":
255 UpdateReleaseTracks(app, release).ServeHTTP(w, r)
256 case "credits":
257 UpdateReleaseCredits(app, release).ServeHTTP(w, r)
258 case "links":
259 UpdateReleaseLinks(app, release).ServeHTTP(w, r)
260 }
261 return
262 }
263
264 if len(segments) > 2 {
265 http.NotFound(w, r)
266 return
267 }
268
269 err := json.NewDecoder(r.Body).Decode(&release)
270 if err != nil {
271 fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err)
272 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
273 return
274 }
275
276 if release.Artwork == "" {
277 release.Artwork = "/img/default-cover-art.png"
278 } else {
279 if strings.Contains(release.Artwork, ";base64,") {
280 var artworkDirectory = filepath.Join("uploads", "musicart")
281 filename, err := HandleImageUpload(app, &release.Artwork, artworkDirectory, release.ID)
282
283 // clean up files with this ID and different extensions
284 err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error {
285 if path == filepath.Join(artworkDirectory, filename) { return nil }
286
287 withoutExt := strings.TrimSuffix(path, filepath.Ext(path))
288 if withoutExt != filepath.Join(artworkDirectory, release.ID) { return nil }
289
290 return os.Remove(path)
291 })
292 if err != nil {
293 fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err)
294 }
295
296 release.Artwork = fmt.Sprintf("/uploads/musicart/%s", filename)
297 }
298 }
299
300 err = controller.UpdateRelease(app.DB, release)
301 if err != nil {
302 if strings.Contains(err.Error(), "no rows") {
303 http.NotFound(w, r)
304 return
305 }
306 fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err)
307 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
308 }
309
310 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
311 })
312}
313
314func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handler {
315 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
316 session := r.Context().Value("session").(*model.Session)
317
318 var trackIDs = []string{}
319 err := json.NewDecoder(r.Body).Decode(&trackIDs)
320 if err != nil {
321 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
322 return
323 }
324
325 err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
326 if err != nil {
327 if strings.Contains(err.Error(), "no rows") {
328 http.NotFound(w, r)
329 return
330 }
331 fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
332 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
333 }
334
335 app.Log.Info(log.TYPE_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
336 })
337}
338
339func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Handler {
340 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
341 session := r.Context().Value("session").(*model.Session)
342
343 type creditJSON struct {
344 Artist string
345 Role string
346 Primary bool
347 }
348 var data []creditJSON
349 err := json.NewDecoder(r.Body).Decode(&data)
350 if err != nil {
351 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
352 return
353 }
354
355 var credits []*model.Credit
356 for _, credit := range data {
357 credits = append(credits, &model.Credit{
358 Artist: model.Artist{
359 ID: credit.Artist,
360 },
361 Role: credit.Role,
362 Primary: credit.Primary,
363 })
364 }
365
366 err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
367 if err != nil {
368 if strings.Contains(err.Error(), "duplicate key") {
369 http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
370 return
371 }
372 if strings.Contains(err.Error(), "no rows") {
373 http.NotFound(w, r)
374 return
375 }
376 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
377 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
378 }
379
380 app.Log.Info(log.TYPE_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
381 })
382}
383
384func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handler {
385 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
386 session := r.Context().Value("session").(*model.Session)
387
388 var links = []*model.Link{}
389 err := json.NewDecoder(r.Body).Decode(&links)
390 if err != nil {
391 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
392 return
393 }
394
395 err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
396 if err != nil {
397 if strings.Contains(err.Error(), "no rows") {
398 http.NotFound(w, r)
399 return
400 }
401 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
402 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
403 }
404
405 app.Log.Info(log.TYPE_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
406 })
407}
408
409func DeleteRelease(app *model.AppState, release *model.Release) http.Handler {
410 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
411 session := r.Context().Value("session").(*model.Session)
412
413 err := controller.DeleteRelease(app.DB, release.ID)
414 if err != nil {
415 if strings.Contains(err.Error(), "no rows") {
416 http.NotFound(w, r)
417 return
418 }
419 fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err)
420 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
421 }
422
423 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username)
424 })
425}