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(), "duplicate key") {
328 http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
329 return
330 }
331 if strings.Contains(err.Error(), "no rows") {
332 http.NotFound(w, r)
333 return
334 }
335 fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
336 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
337 }
338
339 app.Log.Info(log.TYPE_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
340 })
341}
342
343func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Handler {
344 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
345 session := r.Context().Value("session").(*model.Session)
346
347 type creditJSON struct {
348 Artist string
349 Role string
350 Primary bool
351 }
352 var data []creditJSON
353 err := json.NewDecoder(r.Body).Decode(&data)
354 if err != nil {
355 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
356 return
357 }
358
359 var credits []*model.Credit
360 for _, credit := range data {
361 credits = append(credits, &model.Credit{
362 Artist: model.Artist{
363 ID: credit.Artist,
364 },
365 Role: credit.Role,
366 Primary: credit.Primary,
367 })
368 }
369
370 err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
371 if err != nil {
372 if strings.Contains(err.Error(), "duplicate key") {
373 http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
374 return
375 }
376 if strings.Contains(err.Error(), "no rows") {
377 http.NotFound(w, r)
378 return
379 }
380 fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err)
381 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
382 }
383
384 app.Log.Info(log.TYPE_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
385 })
386}
387
388func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handler {
389 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
390 session := r.Context().Value("session").(*model.Session)
391
392 var links = []*model.Link{}
393 err := json.NewDecoder(r.Body).Decode(&links)
394 if err != nil {
395 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
396 return
397 }
398
399 err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
400 if err != nil {
401 if strings.Contains(err.Error(), "duplicate key") {
402 http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
403 return
404 }
405 if strings.Contains(err.Error(), "no rows") {
406 http.NotFound(w, r)
407 return
408 }
409 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
410 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
411 }
412
413 app.Log.Info(log.TYPE_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
414 })
415}
416
417func DeleteRelease(app *model.AppState, release *model.Release) http.Handler {
418 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
419 session := r.Context().Value("session").(*model.Session)
420
421 err := controller.DeleteRelease(app.DB, release.ID)
422 if err != nil {
423 if strings.Contains(err.Error(), "no rows") {
424 http.NotFound(w, r)
425 return
426 }
427 fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err)
428 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
429 }
430
431 app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username)
432 })
433}