HEAVY: finish music service migration, tidy up services, more tests

This commit is contained in:
ari melody 2026-08-01 00:29:31 +01:00
parent 9e311df462
commit 90a671982c
Signed by: ari
GPG key ID: CF99829C92678188
47 changed files with 2698 additions and 902 deletions

View file

@ -6,7 +6,6 @@ import (
"strings" "strings"
"arimelody-web/admin/templates" "arimelody-web/admin/templates"
"arimelody-web/controller"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
) )
@ -23,7 +22,7 @@ func serveArtists(app *app.AppState) http.Handler {
return return
} }
artists, err := controller.GetAllArtists(app.DB) artists, err := app.MusicService.GetAllArtists()
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch artists: %s\n", err) fmt.Printf("WARN: Failed to fetch artists: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -50,7 +49,7 @@ func serveArtist(app *app.AppState, artistID string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
artist, err := controller.GetArtist(app.DB, artistID) artist, err := app.MusicService.GetArtistByID(artistID)
if err != nil { if err != nil {
if artist == nil { if artist == nil {
http.NotFound(w, r) http.NotFound(w, r)
@ -61,7 +60,7 @@ func serveArtist(app *app.AppState, artistID string) http.Handler {
return return
} }
credits, err := controller.GetArtistCredits(app.DB, artist.ID, true) credits, err := app.MusicService.GetArtistCredits(artistID, true)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err) fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

View file

@ -83,39 +83,39 @@ func AdminIndexHandler(app *app.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
releases, err := controller.GetAllReleases(app.DB, false, 3, true) releases, err := app.MusicService.GetAllReleases(false, 3)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
releaseCount, err := controller.GetReleaseCount(app.DB, false) releaseCount, err := app.MusicService.GetReleaseCount(false)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
artists, err := controller.GetAllArtists(app.DB) artists, err := app.MusicService.GetAllArtists()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
artistCount, err := controller.GetArtistCount(app.DB) artistCount, err := app.MusicService.GetArtistCount()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
tracks, err := controller.GetOrphanTracks(app.DB) tracks, err := app.MusicService.GetOrphanTracks()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
trackCount, err := controller.GetTrackCount(app.DB) trackCount, err := app.MusicService.GetTrackCount()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

View file

@ -7,9 +7,9 @@ import (
"strings" "strings"
"arimelody-web/admin/templates" "arimelody-web/admin/templates"
"arimelody-web/controller"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
"arimelody-web/errors"
) )
func serveReleases(app *app.AppState) http.Handler { func serveReleases(app *app.AppState) http.Handler {
@ -34,7 +34,7 @@ func serveReleases(app *app.AppState) http.Handler {
Releases []*model.Release Releases []*model.Release
} }
releases, err := controller.GetAllReleases(app.DB, false, 0, true) releases, err := app.MusicService.GetAllFullReleases(false, 0)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -60,9 +60,13 @@ func serveRelease(app *app.AppState, releaseID string, action string) http.Handl
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
release, err := controller.GetRelease(app.DB, releaseID, true) release, err := app.MusicService.GetFullReleaseByID(releaseID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -130,7 +134,7 @@ func serveEditCredits(release *model.Release) http.Handler {
func serveAddCredit(app *app.AppState, release *model.Release) http.Handler { func serveAddCredit(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID) artists, err := app.MusicService.GetArtistsNotOnRelease(release.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err) fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -158,7 +162,7 @@ func serveNewCredit(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path, "/") split := strings.Split(r.URL.Path, "/")
artistID := split[len(split) - 1] artistID := split[len(split) - 1]
artist, err := controller.GetArtist(app.DB, artistID) artist, err := app.MusicService.GetArtistByID(artistID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err) fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -207,7 +211,7 @@ func serveEditTracks(release *model.Release) http.Handler {
func serveAddTrack(app *app.AppState, release *model.Release) http.Handler { func serveAddTrack(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID) tracks, err := app.MusicService.GetTracksNotOnRelease(release.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err) fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -235,7 +239,7 @@ func serveNewTrack(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
split := strings.Split(r.URL.Path, "/") split := strings.Split(r.URL.Path, "/")
trackID := split[len(split) - 1] trackID := split[len(split) - 1]
track, err := controller.GetTrack(app.DB, trackID) track, err := app.MusicService.GetTrackByID(trackID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err) fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

View file

@ -6,7 +6,6 @@ import (
"strings" "strings"
"arimelody-web/admin/templates" "arimelody-web/admin/templates"
"arimelody-web/controller"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
) )
@ -23,7 +22,7 @@ func serveTracks(app *app.AppState) http.Handler {
return return
} }
tracks, err := controller.GetAllTracks(app.DB) tracks, err := app.MusicService.GetAllTracks()
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch tracks: %s\n", err) fmt.Printf("WARN: Failed to fetch tracks: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -50,7 +49,7 @@ func serveTrack(app *app.AppState, trackID string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
track, err := controller.GetTrack(app.DB, trackID) track, err := app.MusicService.GetTrackByID(trackID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err) fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -61,7 +60,7 @@ func serveTrack(app *app.AppState, trackID string) http.Handler {
return return
} }
releases, err := controller.GetTrackReleases(app.DB, track.ID, true) releases, err := app.MusicService.GetTrackReleases(trackID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err) fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

View file

@ -10,6 +10,7 @@ import (
"arimelody-web/controller" "arimelody-web/controller"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
"arimelody-web/errors"
) )
func Handler(app *app.AppState) http.Handler { func Handler(app *app.AppState) http.Handler {
@ -21,9 +22,9 @@ func Handler(app *app.AppState) http.Handler {
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artistID = strings.Split(r.URL.Path[1:], "/")[0] var artistID = strings.Split(r.URL.Path[1:], "/")[0]
artist, err := controller.GetArtist(app.DB, artistID) artist, err := app.MusicService.GetArtistByID(artistID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -63,9 +64,9 @@ func Handler(app *app.AppState) http.Handler {
mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var releaseID = strings.Split(r.URL.Path[1:], "/")[0] var releaseID = strings.Split(r.URL.Path[1:], "/")[0]
release, err := controller.GetRelease(app.DB, releaseID, true) release, err := app.MusicService.GetFullReleaseByID(releaseID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -105,9 +106,9 @@ func Handler(app *app.AppState) http.Handler {
mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var trackID = strings.Split(r.URL.Path[1:], "/")[0] var trackID = strings.Split(r.URL.Path[1:], "/")[0]
track, err := controller.GetTrack(app.DB, trackID) track, err := app.MusicService.GetTrackByID(trackID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -187,7 +188,10 @@ func getSession(app *app.AppState, r *http.Request) (*model.Session, error) {
// fetch existing session // fetch existing session
session, err := controller.GetSession(app, token) session, err := controller.GetSession(app, token)
if err != nil && !strings.Contains(err.Error(), "no rows") { if errors.IsValidationError(err) {
return nil, err
}
if errors.IsNotExistError(err) {
return nil, fmt.Errorf("Failed to retrieve session: %v\n", err) return nil, fmt.Errorf("Failed to retrieve session: %v\n", err)
} }

View file

@ -10,15 +10,15 @@ import (
"strings" "strings"
"time" "time"
"arimelody-web/controller"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
"arimelody-web/errors"
) )
func ServeAllArtists(app *app.AppState) http.Handler { func ServeAllArtists(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artists = []*model.Artist{} var artists = []*model.Artist{}
artists, err := controller.GetAllArtists(app.DB) artists, err := app.MusicService.GetAllArtists()
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve all artists: %s\n", err) fmt.Printf("WARN: Failed to serve all artists: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -53,9 +53,9 @@ func ServeArtist(app *app.AppState, artist *model.Artist) http.Handler {
) )
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
show_hidden_releases := session != nil && session.Account != nil showHiddenReleases := session != nil && session.Account != nil
dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases) dbCredits, err := app.MusicService.GetArtistCredits(artist.ID, showHiddenReleases)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err) fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -91,31 +91,34 @@ func CreateArtist(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
var artist model.Artist type CreateArtistDTO struct {
err := json.NewDecoder(r.Body).Decode(&artist) ID string `json:"id"`
Name string `json:"name"`
}
dto := &CreateArtistDTO{}
err := json.NewDecoder(r.Body).Decode(dto)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
if artist.ID == "" { err = app.MusicService.CreateArtist(dto.ID, dto.Name, "", "")
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
return
}
if artist.Name == "" { artist.Name = artist.ID }
err = controller.CreateArtist(app.DB, &artist)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "duplicate key") { if errors.IsValidationError(err) {
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
fmt.Printf("WARN: Failed to create artist %s: %s\n", artist.ID, err) if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, fmt.Sprintf("Artist %s already exists\n", dto.ID), http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to create artist %s: %s\n", dto.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" created by \"%s\".", artist.Name, session.Account.Username) app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" created by \"%s\".", dto.Name, session.Account.Username)
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
}) })
@ -156,9 +159,13 @@ func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler {
} }
} }
err = controller.UpdateArtist(app.DB, artist) err = app.MusicService.UpdateArtist(artist)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -174,9 +181,13 @@ func DeleteArtist(app *app.AppState, artist *model.Artist) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
err := controller.DeleteArtist(app.DB, artist.ID) err := app.MusicService.DeleteArtist(artist.ID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }

View file

@ -13,6 +13,7 @@ import (
"arimelody-web/controller" "arimelody-web/controller"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
"arimelody-web/errors"
) )
func ServeRelease(app *app.AppState, release *model.Release) http.Handler { func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
@ -39,50 +40,50 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
} }
type ( type (
Track struct { TrackDTO struct {
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
Lyrics string `json:"lyrics"` Lyrics string `json:"lyrics"`
} }
Credit struct { CreditDTO struct {
*model.Artist *model.Artist
Role string `json:"role"` Role string `json:"role"`
Primary bool `json:"primary"` Primary bool `json:"primary"`
} }
Release struct { ReleaseDTO struct {
*model.Release *model.Release
Tracks []Track `json:"tracks"` Tracks []TrackDTO `json:"tracks"`
Credits []Credit `json:"credits"` Credits []CreditDTO `json:"credits"`
Links map[string]string `json:"links"` Links map[string]string `json:"links"`
} }
) )
response := Release{ response := ReleaseDTO{
Release: release, Release: release,
Tracks: []Track{}, Tracks: []TrackDTO{},
Credits: []Credit{}, Credits: []CreditDTO{},
Links: make(map[string]string), Links: make(map[string]string),
} }
if release.IsReleased() || privileged { if release.IsReleased() || privileged {
// get credits // get credits
credits, err := controller.GetReleaseCredits(app.DB, release.ID) credits, err := app.MusicService.GetReleaseCredits(release.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err) fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
for _, credit := range credits { for _, credit := range credits {
artist, err := controller.GetArtist(app.DB, credit.Artist.ID) artist, err := app.MusicService.GetArtistByID(credit.Artist.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err) fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
response.Credits = append(response.Credits, Credit{ response.Credits = append(response.Credits, CreditDTO{
Artist: artist, Artist: artist,
Role: credit.Role, Role: credit.Role,
Primary: credit.Primary, Primary: credit.Primary,
@ -90,14 +91,14 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
} }
// get tracks // get tracks
tracks, err := controller.GetReleaseTracks(app.DB, release.ID) tracks, err := app.MusicService.GetReleaseTracks(release.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err) fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
for _, track := range tracks { for _, track := range tracks {
response.Tracks = append(response.Tracks, Track{ response.Tracks = append(response.Tracks, TrackDTO{
Title: track.Title, Title: track.Title,
Description: track.Description, Description: track.Description,
Lyrics: track.Lyrics, Lyrics: track.Lyrics,
@ -105,7 +106,7 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
} }
// get links // get links
links, err := controller.GetReleaseLinks(app.DB, release.ID) links, err := app.MusicService.GetReleaseLinks(release.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err) fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -129,7 +130,7 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
func ServeCatalog(app *app.AppState) http.Handler { func ServeCatalog(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
releases, err := controller.GetAllReleases(app.DB, false, 0, true) releases, err := app.MusicService.GetAllFullReleases(false, 0)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
@ -192,47 +193,56 @@ func CreateRelease(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
var release model.Release type CreateReleaseDTO struct {
err := json.NewDecoder(r.Body).Decode(&release) ID string `json:"id"`
Title string `json:"title"`
ReleaseType string `json:"type"`
ReleaseDate time.Time `json:"release_date"`
Artwork string `json:"artwork"`
}
var dto CreateReleaseDTO
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
if release.ID == "" { if dto.ReleaseType == "" { dto.ReleaseType = string(model.Single) }
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) if dto.ReleaseDate != time.Unix(0, 0) {
return dto.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
} }
if dto.Artwork == "" { dto.Artwork = model.DEFAULT_RELEASE_ARTWORK_URL }
if release.Title == "" { release.Title = release.ID } err = app.MusicService.CreateRelease(
if release.ReleaseType == "" { release.ReleaseType = model.Single } dto.ID,
dto.Title,
if release.ReleaseDate != time.Unix(0, 0) { dto.ReleaseType,
release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) dto.ReleaseDate,
} dto.Artwork,
)
if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" }
err = controller.CreateRelease(app.DB, &release)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "duplicate key") { if errors.IsValidationError(err) {
http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err) if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, fmt.Sprintf("Release %s already exists\n", dto.ID), http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to create release %s: %s\n", dto.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" created by \"%s\".", release.ID, session.Account.Username) app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" created by \"%s\".", dto.ID, session.Account.Username)
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t") encoder.SetIndent("", "\t")
err = encoder.Encode(release) err = encoder.Encode(dto)
if err != nil { if err != nil {
fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err) fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", dto.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
}) })
@ -297,9 +307,13 @@ func UpdateRelease(app *app.AppState, release *model.Release) http.Handler {
} }
} }
err = controller.UpdateRelease(app.DB, release) err = app.MusicService.UpdateRelease(release)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@ -315,28 +329,32 @@ func UpdateReleaseTracks(app *app.AppState, release *model.Release) http.Handler
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
var trackIDs = []string{} var newTrackIDs = []string{}
err := json.NewDecoder(r.Body).Decode(&trackIDs) err := json.NewDecoder(r.Body).Decode(&newTrackIDs)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs) err = app.MusicService.UpdateReleaseTracks(release.ID, newTrackIDs)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "duplicate key") { if errors.IsValidationError(err) {
http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if strings.Contains(err.Error(), "no rows") { if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err) fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
app.LogService.Info(model.LOG_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" tracklist updated by \"%s\".", release.ID, session.Account.Username)
}) })
} }
@ -344,22 +362,22 @@ func UpdateReleaseCredits(app *app.AppState, release *model.Release) http.Handle
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
type creditJSON struct { type CreditDTO struct {
Artist string Artist string
Role string Role string
Primary bool Primary bool
} }
var data []creditJSON var dto []CreditDTO
err := json.NewDecoder(r.Body).Decode(&data) err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
var credits []*model.Credit var newCredits []*model.Credit
for _, credit := range data { for _, credit := range dto {
credits = append(credits, &model.Credit{ newCredits = append(newCredits, &model.Credit{
Artist: model.Artist{ Artist: &model.Artist{
ID: credit.Artist, ID: credit.Artist,
}, },
Role: credit.Role, Role: credit.Role,
@ -367,21 +385,25 @@ func UpdateReleaseCredits(app *app.AppState, release *model.Release) http.Handle
}) })
} }
err = controller.UpdateReleaseCredits(app.DB, release.ID, credits) err = app.MusicService.UpdateReleaseCredits(release.ID, newCredits)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "duplicate key") { if errors.IsValidationError(err) {
http.Error(w, "Artists may only be credited once", http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if strings.Contains(err.Error(), "no rows") { if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err) fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
app.LogService.Info(model.LOG_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" credits updated by \"%s\".", release.ID, session.Account.Username)
}) })
} }
@ -389,28 +411,41 @@ func UpdateReleaseLinks(app *app.AppState, release *model.Release) http.Handler
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
var links = []*model.Link{} type LinkDTO struct {
err := json.NewDecoder(r.Body).Decode(&links) Name string `json:"name"`
URL string `json:"url"`
}
var dto = []LinkDTO{}
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
err = controller.UpdateReleaseLinks(app.DB, release.ID, links) links := []*model.Link{}
for _, link := range dto {
links = append(links, &model.Link{ Name: link.Name, URL: link.URL })
}
err = app.MusicService.UpdateReleaseLinks(release.ID, links)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "duplicate key") { if errors.IsValidationError(err) {
http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if strings.Contains(err.Error(), "no rows") { if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if strings.Contains(err.Error(), "duplicate key") {
http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
return
}
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
app.LogService.Info(model.LOG_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" links updated by \"%s\".", release.ID, session.Account.Username)
}) })
} }
@ -418,9 +453,13 @@ func DeleteRelease(app *app.AppState, release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
err := controller.DeleteRelease(app.DB, release.ID) err := app.MusicService.DeleteRelease(release.ID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if errors.IsNotExistError(err) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }

View file

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"arimelody-web/controller" "arimelody-web/errors"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
) )
@ -26,7 +26,7 @@ func ServeAllTracks(app *app.AppState) http.Handler {
var tracks = []Track{} var tracks = []Track{}
var dbTracks = []*model.Track{} var dbTracks = []*model.Track{}
dbTracks, err := controller.GetAllTracks(app.DB) dbTracks, err := app.MusicService.GetAllTracks()
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err) fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -52,7 +52,7 @@ func ServeAllTracks(app *app.AppState) http.Handler {
func ServeTrack(app *app.AppState, track *model.Track) http.Handler { func ServeTrack(app *app.AppState, track *model.Track) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dbReleases, err := controller.GetTrackReleases(app.DB, track.ID, false) dbReleases, err := app.MusicService.GetTrackReleases(track.ID)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err) fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -78,26 +78,30 @@ func CreateTrack(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
var track model.Track type CreateTrackDTO struct {
err := json.NewDecoder(r.Body).Decode(&track) Title string `json:"title"`
Description string `json:"description"`
Lyrics string `json:"lyrics"`
}
var dto CreateTrackDTO
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
if track.Title == "" { id, err := app.MusicService.CreateTrack(dto.Title, dto.Description, dto.Lyrics, "")
http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
id, err := controller.CreateTrack(app.DB, &track)
if err != nil {
fmt.Printf("WARN: Failed to create track: %s\n", err) fmt.Printf("WARN: Failed to create track: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) created by \"%s\".", track.Title, track.ID, session.Account.Username) app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) created by \"%s\".", dto.Title, id, session.Account.Username)
w.Header().Add("Content-Type", "text/plain") w.Header().Add("Content-Type", "text/plain")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
@ -114,19 +118,28 @@ func UpdateTrack(app *app.AppState, track *model.Track) http.Handler {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
err := json.NewDecoder(r.Body).Decode(&track) type UpdateTrackDTO struct {
Title string `json:"title"`
Description string `json:"description"`
Lyrics string `json:"lyrics"`
}
var dto UpdateTrackDTO
err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
if track.Title == "" { track.Title = dto.Title
http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) track.Description = dto.Description
track.Lyrics = dto.Lyrics
err = app.MusicService.UpdateTrack(track)
if err != nil {
if errors.IsValidationError(err) {
http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
err = controller.UpdateTrack(app.DB, track)
if err != nil {
fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err) fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
@ -154,8 +167,12 @@ func DeleteTrack(app *app.AppState, track *model.Track) http.Handler {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
var trackID = r.URL.Path[1:] var trackID = r.URL.Path[1:]
err := controller.DeleteTrack(app.DB, trackID) err := app.MusicService.DeleteTrack(trackID)
if err != nil { if err != nil {
if errors.IsNotExistError(err) {
http.NotFound(w, r)
return
}
fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err) fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }

View file

@ -1,327 +0,0 @@
package controller
import (
"fmt"
"arimelody-web/model"
"github.com/jmoiron/sqlx"
)
func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
var release = model.Release{}
err := db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
if err != nil {
return nil, err
}
if full {
// get credits
credits, err := GetReleaseCredits(db, id)
if err != nil {
return nil, fmt.Errorf("Credits: %s", err)
}
for _, credit := range credits {
release.Credits = append(release.Credits, credit)
}
// get tracks
tracks, err := GetReleaseTracks(db, id)
if err != nil {
return nil, fmt.Errorf("Tracks: %s", err)
}
for _, track := range tracks {
release.Tracks = append(release.Tracks, track)
}
// get links
links, err := GetReleaseLinks(db, id)
if err != nil {
return nil, fmt.Errorf("Links: %s", err)
}
for _, link := range links {
release.Links = append(release.Links, link)
}
}
return &release, nil
}
func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*model.Release, error) {
var releases = []*model.Release{}
query := "SELECT * FROM musicrelease"
if onlyVisible {
query += " WHERE visible=true"
}
query += " ORDER BY release_date DESC"
var err error
if limit > 0 {
err = db.Select(&releases, query + " LIMIT $1", limit)
} else {
err = db.Select(&releases, query)
}
if err != nil {
return nil, err
}
for _, release := range releases {
// get credits
credits, err := GetReleaseCredits(db, release.ID)
if err != nil {
return nil, fmt.Errorf("Credits: %s", err)
}
for _, credit := range credits {
release.Credits = append(release.Credits, credit)
}
if full {
// get tracks
tracks, err := GetReleaseTracks(db, release.ID)
if err != nil {
return nil, fmt.Errorf("Tracks: %s", err)
}
for _, track := range tracks {
release.Tracks = append(release.Tracks, track)
}
// get links
links, err := GetReleaseLinks(db, release.ID)
if err != nil {
return nil, fmt.Errorf("Links: %s", err)
}
for _, link := range links {
release.Links = append(release.Links, link)
}
}
}
return releases, nil
}
func GetReleaseCount(db *sqlx.DB, onlyVisible bool) (int, error) {
query := "SELECT count(*) FROM musicrelease"
if onlyVisible {
query += " WHERE visible=true"
}
var count int
err := db.Get(&count, query)
return count, err
}
func CreateRelease(db *sqlx.DB, release *model.Release) error {
_, err := db.Exec(
"INSERT INTO musicrelease "+
"(id, visible, title, description, type, release_date, artwork, buyname, buylink, copyright, copyrighturl) "+
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
release.ID,
release.Visible,
release.Title,
release.Description,
release.ReleaseType,
release.ReleaseDate.Format("2006-01-02 15:04:05"),
release.Artwork,
release.Buyname,
release.Buylink,
release.Copyright,
release.CopyrightURL,
)
if err != nil {
return err
}
return nil
}
func UpdateRelease(db *sqlx.DB, release *model.Release) error {
_, err := db.Exec(
"UPDATE musicrelease SET "+
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+
"WHERE id=$1",
release.ID,
release.Visible,
release.Title,
release.Description,
release.ReleaseType,
release.ReleaseDate.Format("2006-01-02 15:04:05"),
release.Artwork,
release.Buyname,
release.Buylink,
release.Copyright,
release.CopyrightURL,
)
if err != nil {
return err
}
return nil
}
func UpdateReleaseTracks(db *sqlx.DB, releaseID string, new_tracks []string) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
if err != nil {
return err
}
for i, trackID := range new_tracks {
_, err = tx.Exec(
"INSERT INTO musicreleasetrack "+
"(release, track, number) "+
"VALUES ($1, $2, $3)",
releaseID,
trackID,
i)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Credit) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
if err != nil {
return err
}
for _, credit := range new_credits {
_, err = tx.Exec(
"INSERT INTO musiccredit "+
"(release, artist, role, is_primary) "+
"VALUES ($1, $2, $3, $4)",
releaseID,
credit.Artist.ID,
credit.Role,
credit.Primary,
)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
if err != nil {
return err
}
for _, link := range new_links {
_, err := tx.Exec(
"INSERT INTO musiclink "+
"(release, name, url) "+
"VALUES ($1, $2, $3)",
releaseID,
link.Name,
link.URL,
)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func DeleteRelease(db *sqlx.DB, releaseID string) error {
_, err := db.Exec(
"DELETE FROM musicrelease "+
"WHERE id=$1",
releaseID,
)
if err != nil {
return err
}
return nil
}
func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := db.Select(&tracks,
"SELECT musictrack.* FROM musictrack "+
"JOIN musicreleasetrack ON track=id "+
"WHERE release=$1 "+
"ORDER BY number ASC",
releaseID,
)
if err != nil {
return nil, err
}
return tracks, nil
}
func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) {
rows, err := db.Query(
"SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
"FROM musiccredit "+
"JOIN artist ON artist=artist.id "+
"JOIN musicrelease ON release=musicrelease.id "+
"WHERE musicrelease.id=$1 "+
"ORDER BY is_primary DESC",
releaseID,
)
if err != nil {
return nil, err
}
var credits []*model.Credit
for rows.Next() {
credit := model.Credit{}
rows.Scan(
&credit.Artist.ID,
&credit.Artist.Name,
&credit.Artist.Website,
&credit.Artist.Avatar,
&credit.Role,
&credit.Primary)
credits = append(credits, &credit)
}
return credits, nil
}
func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) {
var links = []*model.Link{}
err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID)
if err != nil {
return nil, err
}
return links, nil
}

View file

@ -1,181 +0,0 @@
package controller
import (
"arimelody-web/model"
"github.com/jmoiron/sqlx"
)
// DATABASE
func GetTrack(db *sqlx.DB, id string) (*model.Track, error) {
var track = model.Track{}
stmt, _ := db.Preparex("SELECT * FROM musictrack WHERE id=$1")
err := stmt.Get(&track, id)
if err != nil {
return nil, err
}
return &track, nil
}
func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := db.Select(&tracks, "SELECT * FROM musictrack")
if err != nil {
return nil, err
}
return tracks, nil
}
func GetTrackCount(db *sqlx.DB) (int, error) {
var count int
err := db.Get(&count, "SELECT count(*) FROM musictrack")
return count, err
}
func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)")
if err != nil {
return nil, err
}
return tracks, nil
}
func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := db.Select(&tracks,
"SELECT * FROM musictrack "+
"WHERE id NOT IN "+
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
releaseID)
if err != nil {
return nil, err
}
return tracks, nil
}
func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) {
var releases = []*model.Release{}
err := db.Select(&releases,
"SELECT id,title,type,release_date,artwork,buylink "+
"FROM musicrelease "+
"JOIN musicreleasetrack ON release=id "+
"WHERE track=$1 "+
"ORDER BY release_date",
trackID,
)
if err != nil {
return nil, err
}
type NamePrimary struct {
Name string `json:"name"`
Primary bool `json:"primary" db:"is_primary"`
}
for _, release := range releases {
// get artists
credits := []NamePrimary{}
err := db.Select(&credits,
"SELECT name,is_primary FROM artist "+
"JOIN musiccredit ON artist=artist.id "+
"JOIN musicrelease ON release=musicrelease.id "+
"WHERE musicrelease.id=$1", release.ID)
if err != nil {
return nil, err
}
for _, credit := range credits {
release.Credits = append(release.Credits, &model.Credit{
Artist: model.Artist{
Name: credit.Name,
},
Primary: credit.Primary,
})
}
// get tracks
tracks := []string{}
err = db.Select(&tracks, "SELECT track FROM musicreleasetrack WHERE release=$1", release.ID)
if err != nil {
return nil, err
}
for _, trackID := range tracks {
release.Tracks = append(release.Tracks, &model.Track{
ID: trackID,
})
}
}
return releases, nil
}
func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := db.Select(&tracks,
"SELECT id, title, description, lyrics, preview_url FROM musictrack "+
"WHERE id NOT IN "+
"(SELECT track FROM musicreleasetrack)",
)
if err != nil {
return nil, err
}
return tracks, nil
}
func CreateTrack(db *sqlx.DB, track *model.Track) (string, error) {
var trackID string
err := db.QueryRow(
"INSERT INTO musictrack (title, description, lyrics, preview_url) "+
"VALUES ($1, $2, $3, $4) "+
"RETURNING id",
track.Title,
track.Description,
track.Lyrics,
track.PreviewURL,
).Scan(&trackID)
if err != nil {
return "", err
}
return trackID, nil
}
func UpdateTrack(db *sqlx.DB, track *model.Track) error {
_, err := db.Exec(
"UPDATE musictrack "+
"SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+
"WHERE id=$1",
track.ID,
track.Title,
track.Description,
track.Lyrics,
track.PreviewURL,
)
if err != nil {
return err
}
return nil
}
func DeleteTrack(db *sqlx.DB, trackID string) error {
_, err := db.Exec(
"DELETE FROM musictrack "+
"WHERE id=$1",
trackID,
)
if err != nil {
return err
}
return nil
}

16
errors/notexist.go Normal file
View file

@ -0,0 +1,16 @@
package errors
type NotExistError struct {
query string
}
func NewNotExistError(query string) *NotExistError {
return &NotExistError{ query: query }
}
func (err *NotExistError) Error() string {
return err.query
}
func IsNotExistError(err error) bool {
_, ok := err.(*NotExistError)
return ok
}

26
errors/notexist_test.go Normal file
View file

@ -0,0 +1,26 @@
package errors_test
import (
"arimelody-web/errors"
goErrors "errors"
"testing"
"gotest.tools/v3/assert"
)
func Test_NotExistError(t *testing.T) {
var err error
message := "entity does not exist"
t.Run("can create error", func(t *testing.T) {
err = errors.NewNotExistError(message)
assert.Error(t, err, message)
})
t.Run("validator returns true for valid error", func(t *testing.T) {
assert.Equal(t, errors.IsNotExistError(err), true)
})
t.Run("validator returns false for invalid error", func(t *testing.T) {
assert.Equal(t, errors.IsNotExistError(goErrors.New("other error")), false)
})
}

15
errors/validation.go Normal file
View file

@ -0,0 +1,15 @@
package errors
type ValidationError struct {
message string
}
func NewValidationError(message string) *ValidationError {
return &ValidationError{ message: message }
}
func (err *ValidationError) Error() string {
return err.message
}
func IsValidationError(err error) bool {
_, ok := err.(*ValidationError)
return ok
}

26
errors/validation_test.go Normal file
View file

@ -0,0 +1,26 @@
package errors_test
import (
"arimelody-web/errors"
goErrors "errors"
"testing"
"gotest.tools/v3/assert"
)
func Test_ValidationError(t *testing.T) {
var err error
message := "invalid input"
t.Run("can create error", func(t *testing.T) {
err = errors.NewValidationError(message)
assert.Error(t, err, message)
})
t.Run("validator returns true for valid error", func(t *testing.T) {
assert.Equal(t, errors.IsValidationError(err), true)
})
t.Run("validator returns false for invalid error", func(t *testing.T) {
assert.Equal(t, errors.IsValidationError(goErrors.New("other error")), false)
})
}

13
main.go
View file

@ -24,12 +24,16 @@ import (
"arimelody-web/cursor" "arimelody-web/cursor"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/model/app" "arimelody-web/model/app"
"arimelody-web/view"
accountRepo "arimelody-web/repository/account" accountRepo "arimelody-web/repository/account"
logRepo "arimelody-web/repository/log" logRepo "arimelody-web/repository/log"
musicRepo "arimelody-web/repository/music"
repo "arimelody-web/repository/postgres" repo "arimelody-web/repository/postgres"
accountService "arimelody-web/service/account" accountService "arimelody-web/service/account"
logService "arimelody-web/service/log" logService "arimelody-web/service/log"
"arimelody-web/view" musicService "arimelody-web/service/music"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
@ -47,6 +51,7 @@ var publicFS embed.FS
func main() { func main() {
// TODO: switch to a new logger. this one kinda sucks // TODO: switch to a new logger. this one kinda sucks
// i'll be so forreal i might write my own
logger := log.New(os.Stderr, "main", model.DEFAULT_LOG_FLAGS) logger := log.New(os.Stderr, "main", model.DEFAULT_LOG_FLAGS)
logger.Print("made with <3 by ari melody\n\n") logger.Print("made with <3 by ari melody\n\n")
@ -103,6 +108,12 @@ func main() {
log.New(os.Stderr, "account-repo", model.DEFAULT_LOG_FLAGS), log.New(os.Stderr, "account-repo", model.DEFAULT_LOG_FLAGS),
) )
musicRepo := musicRepo.NewMusicRepositoryPostgres(psqlDB)
app.MusicService = musicService.NewMusicService(
musicRepo,
log.New(os.Stderr, "music-repo", model.DEFAULT_LOG_FLAGS),
)
// handle command arguments // handle command arguments
if len(os.Args) > 1 { if len(os.Args) > 1 {
arg := os.Args[1] arg := os.Args[1]

View file

@ -8,12 +8,10 @@ import (
"arimelody-web/model/twitch" "arimelody-web/model/twitch"
logService "arimelody-web/service/log" logService "arimelody-web/service/log"
inviteService "arimelody-web/service/invite" //inviteService "arimelody-web/service/invite"
accountService "arimelody-web/service/account" accountService "arimelody-web/service/account"
sessionService "arimelody-web/service/session" //sessionService "arimelody-web/service/session"
artistService "arimelody-web/service/artist" musicService "arimelody-web/service/music"
releaseService "arimelody-web/service/release"
trackService "arimelody-web/service/track"
) )
type ( type (
@ -55,11 +53,9 @@ type (
PublicFS embed.FS PublicFS embed.FS
LogService *logService.LogService LogService *logService.LogService
InviteService *inviteService.InviteService //InviteService *inviteService.InviteService
AccountService *accountService.AccountService AccountService *accountService.AccountService
SesisonService *sessionService.SessionService //SesisonService *sessionService.SessionService
ArtistService *artistService.ArtistService MusicService *musicService.MusicService
ReleaseService *releaseService.ReleaseService
TrackService *trackService.TrackService
} }
) )

View file

@ -9,9 +9,11 @@ type (
} }
) )
const DEFAULT_AVATAR_URL = "/img/default-avatar.png"
func (artist Artist) GetAvatar() string { func (artist Artist) GetAvatar() string {
if artist.Avatar == "" { if artist.Avatar == "" {
return "/img/default-avatar.png" return DEFAULT_AVATAR_URL
} }
return artist.Avatar return artist.Avatar
} }

View file

@ -1,21 +1,22 @@
package model package model_test
import ( import (
"arimelody-web/model"
"testing" "testing"
) )
func Test_Artist_GetAvatar(t *testing.T) { func Test_Artist_GetAvatar(t *testing.T) {
want := "testavatar.png" want := "testavatar.png"
artist := Artist{ Avatar: want } artist := model.Artist{ Avatar: want }
got := artist.GetAvatar() got := artist.GetAvatar()
if want != got { if want != got {
t.Errorf(`correct value not returned when avatar is populated (want "%s", got "%s")`, want, got) t.Errorf(`correct value not returned when avatar is populated (want "%s", got "%s")`, want, got)
} }
artist = Artist{} artist = model.Artist{}
want = "/img/default-avatar.png" want = model.DEFAULT_AVATAR_URL
got = artist.GetAvatar() got = artist.GetAvatar()
if want != got { if want != got {
t.Errorf(`default value not returned when avatar is empty (want "%s", got "%s")`, want, got) t.Errorf(`default value not returned when avatar is empty (want "%s", got "%s")`, want, got)

View file

@ -2,8 +2,8 @@ package model
type ( type (
Credit struct { Credit struct {
Release Release `json:"release"` Release *Release `json:"release"`
Artist Artist `json:"artist"` Artist *Artist `json:"artist"`
Role string `json:"role"` Role string `json:"role"`
Primary bool `json:"primary" db:"is_primary"` Primary bool `json:"primary" db:"is_primary"`
} }

View file

@ -1,11 +1,12 @@
package model package model_test
import ( import (
"arimelody-web/model"
"testing" "testing"
) )
func Test_Link_NormaliseName(t *testing.T) { func Test_Link_NormaliseName(t *testing.T) {
link := Link{ link := model.Link{
Name: "!c@o#o$l%-^a&w*e(s)o_m=e+-[l{i]n}k-0123456789ABCDEF", Name: "!c@o#o$l%-^a&w*e(s)o_m=e+-[l{i]n}k-0123456789ABCDEF",
} }

View file

@ -34,6 +34,8 @@ const (
EP ReleaseType = "EP" EP ReleaseType = "EP"
Compilation ReleaseType = "compilation" Compilation ReleaseType = "compilation"
Upcoming ReleaseType = "upcoming" Upcoming ReleaseType = "upcoming"
DEFAULT_RELEASE_ARTWORK_URL = "/img/default-cover-art.png"
) )
// GETTERS // GETTERS
@ -52,7 +54,7 @@ func (release Release) PrintReleaseDate() string {
func (release Release) GetArtwork() string { func (release Release) GetArtwork() string {
if release.Artwork == "" { if release.Artwork == "" {
return "/img/default-cover-art.png" return DEFAULT_RELEASE_ARTWORK_URL
} }
return release.Artwork return release.Artwork
} }
@ -93,3 +95,20 @@ func (release Release) PrintArtists(only_primary bool, ampersand bool) string {
return strings.Join(names[:], ", ") return strings.Join(names[:], ", ")
} }
} }
func ValidReleaseType(releaseType string) (ReleaseType, bool) {
switch releaseType {
case "single":
return Single, true
case "album":
return Album, true
case "EP":
return EP, true
case "compilation":
return Compilation, true
case "upcoming":
return Upcoming, true
default:
return "", false
}
}

View file

@ -1,6 +1,7 @@
package model package model_test
import ( import (
"arimelody-web/model"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -10,7 +11,7 @@ import (
func Test_Release(t *testing.T) { func Test_Release(t *testing.T) {
t.Run("prints correct description HTML", func(t *testing.T) { t.Run("prints correct description HTML", func(t *testing.T) {
release := Release{ release := model.Release{
Description: "this is\na test\n<strong>description!</strong>", Description: "this is\na test\n<strong>description!</strong>",
} }
@ -24,7 +25,7 @@ func Test_Release(t *testing.T) {
}) })
t.Run("prints correct release date", func(t *testing.T) { t.Run("prints correct release date", func(t *testing.T) {
release := Release{ release := model.Release{
ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC), ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC),
} }
@ -34,37 +35,37 @@ func Test_Release(t *testing.T) {
t.Run("returns correct artwork", func(t *testing.T) { t.Run("returns correct artwork", func(t *testing.T) {
artwork := "testartwork.png" artwork := "testartwork.png"
release := Release{ Artwork: artwork } release := model.Release{ Artwork: artwork }
assert.Equal(t, release.GetArtwork(), artwork) assert.Equal(t, release.GetArtwork(), artwork)
}) })
t.Run("returns placeholder artwork when empty", func(t *testing.T) { t.Run("returns placeholder artwork when empty", func(t *testing.T) {
release := Release{} release := model.Release{}
assert.Equal(t, release.GetArtwork(), "/img/default-cover-art.png") assert.Equal(t, release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
}) })
t.Run("singles", func(t *testing.T) { t.Run("singles", func(t *testing.T) {
release := Release{ release := model.Release{
Tracks: []*Track{}, Tracks: []*model.Track{},
} }
t.Run("false when no tracks are present", func(t *testing.T) { t.Run("false when no tracks are present", func(t *testing.T) {
assert.Equal(t, release.IsSingle(), false) assert.Equal(t, release.IsSingle(), false)
}) })
release.Tracks = append(release.Tracks, &Track{}) release.Tracks = append(release.Tracks, &model.Track{})
t.Run("true when one track is present", func(t *testing.T) { t.Run("true when one track is present", func(t *testing.T) {
assert.Equal(t, release.IsSingle(), true) assert.Equal(t, release.IsSingle(), true)
}) })
release.Tracks = append(release.Tracks, &Track{}) release.Tracks = append(release.Tracks, &model.Track{})
t.Run("false when >1 tracks are present", func(t *testing.T) { t.Run("false when >1 tracks are present", func(t *testing.T) {
assert.Equal(t, release.IsSingle(), false) assert.Equal(t, release.IsSingle(), false)
}) })
}) })
t.Run("released", func(t *testing.T) { t.Run("released", func(t *testing.T) {
release := Release { release := model.Release {
ReleaseDate: time.Now(), ReleaseDate: time.Now(),
} }
@ -84,23 +85,23 @@ func Test_Release(t *testing.T) {
artist3 := "idk" artist3 := "idk"
artist4 := "guest" artist4 := "guest"
release := Release{} release := model.Release{}
t.Run("prints \"Unknown Artist\" when release has no credits", func(t *testing.T) { t.Run("prints \"Unknown Artist\" when release has no credits", func(t *testing.T) {
assert.Equal(t, release.PrintArtists(false, true), "Unknown Artist") assert.Equal(t, release.PrintArtists(false, true), "Unknown Artist")
}) })
release.Credits = append( release.Credits = append(
release.Credits, release.Credits,
&Credit{ Artist: Artist{ Name: artist1 }, Primary: true }, &model.Credit{ Artist: &model.Artist{ Name: artist1 }, Primary: true },
) )
t.Run("prints ONLY first artist name when release has one credit", func(t *testing.T) { t.Run("prints ONLY first artist name when release has one credit", func(t *testing.T) {
assert.Equal(t, release.PrintArtists(false, true), artist1) assert.Equal(t, release.PrintArtists(false, true), artist1)
}) })
release.Credits = append(release.Credits, []*Credit{ release.Credits = append(release.Credits, []*model.Credit{
{ Artist: Artist{ Name: artist2 }, Primary: true }, { Artist: &model.Artist{ Name: artist2 }, Primary: true },
{ Artist: Artist{ Name: artist3 }, Primary: false }, { Artist: &model.Artist{ Name: artist3 }, Primary: false },
{ Artist: Artist{ Name: artist4 }, Primary: true }, { Artist: &model.Artist{ Name: artist4 }, Primary: true },
}...) }...)
t.Run("can get only unique primary artist names", func(t *testing.T) { t.Run("can get only unique primary artist names", func(t *testing.T) {
assert.Equal( assert.Equal(
@ -146,4 +147,37 @@ func Test_Release(t *testing.T) {
) )
}) })
}) })
t.Run("validating release types", func(t *testing.T) {
t.Run("single", func(t *testing.T) {
releaseType, ok := model.ValidReleaseType("single")
assert.Equal(t, ok, true)
assert.Equal(t, releaseType, model.Single)
})
t.Run("album", func(t *testing.T) {
releaseType, ok := model.ValidReleaseType("album")
assert.Equal(t, ok, true)
assert.Equal(t, releaseType, model.Album)
})
t.Run("EP", func(t *testing.T) {
releaseType, ok := model.ValidReleaseType("EP")
assert.Equal(t, ok, true)
assert.Equal(t, releaseType, model.EP)
})
t.Run("compilation", func(t *testing.T) {
releaseType, ok := model.ValidReleaseType("compilation")
assert.Equal(t, ok, true)
assert.Equal(t, releaseType, model.Compilation)
})
t.Run("upcoming", func(t *testing.T) {
releaseType, ok := model.ValidReleaseType("upcoming")
assert.Equal(t, ok, true)
assert.Equal(t, releaseType, model.Upcoming)
})
t.Run("invalid", func(t *testing.T) {
releaseType, ok := model.ValidReleaseType("invalid")
assert.Equal(t, ok, false)
assert.Equal(t, string(releaseType), "")
})
})
} }

View file

@ -1,11 +1,12 @@
package model package model_test
import ( import (
"arimelody-web/model"
"testing" "testing"
) )
func Test_Track_DescriptionHTML(t *testing.T) { func Test_Track_DescriptionHTML(t *testing.T) {
track := Track{ track := model.Track{
Description: "this is\na test\n<strong>description!</strong>", Description: "this is\na test\n<strong>description!</strong>",
} }
@ -19,7 +20,7 @@ func Test_Track_DescriptionHTML(t *testing.T) {
} }
func Test_Track_LyricsHTML(t *testing.T) { func Test_Track_LyricsHTML(t *testing.T) {
track := Track{ track := model.Track{
Lyrics: "these are\ntest\n<strong>lyrics!</strong>", Lyrics: "these are\ntest\n<strong>lyrics!</strong>",
} }

View file

@ -27,11 +27,11 @@ type AccountRepository interface {
UpdateAvatarURL(id string, avatarURL string) error UpdateAvatarURL(id string, avatarURL string) error
RemoveAvatar(id string) error RemoveAvatar(id string) error
Delete(id string) error
// Increment the number of account login failure attempts, // Increment the number of account login failure attempts,
// returning the current fail count. // returning the current fail count.
IncrementFails(id string) (int, error) IncrementFails(id string) (int, error)
ResetFails(id string) error ResetFails(id string) error
SetLocked(id string, lock bool) error SetLocked(id string, lock bool) error
Delete(id string) (string, error)
} }

View file

@ -1,9 +1,10 @@
package account package account
import ( import (
"arimelody-web/errors"
"arimelody-web/model" "arimelody-web/model"
"database/sql" "database/sql"
"errors" "slices"
"strconv" "strconv"
) )
@ -24,10 +25,11 @@ func (repo *AccountRepositoryMemory) GetCount() (int, error) {
return len(repo.accounts), nil return len(repo.accounts), nil
} }
func (repo *AccountRepositoryMemory) GetByID(id string) (*model.Account, error) { func (repo *AccountRepositoryMemory) GetByID(id string) (*model.Account, error) {
for _, account := range repo.accounts { index := slices.IndexFunc(repo.accounts, func(account *model.Account) bool {
if account.ID == id { return account, nil } return account.ID == id
} })
return nil, nil if index == -1 { return nil, nil }
return repo.accounts[index], nil
} }
func (repo *AccountRepositoryMemory) GetByUsername(username string) (*model.Account, error) { func (repo *AccountRepositoryMemory) GetByUsername(username string) (*model.Account, error) {
for _, account := range repo.accounts { for _, account := range repo.accounts {
@ -47,9 +49,9 @@ func (repo *AccountRepositoryMemory) GetByEmail(email string) (*model.Account, e
// Create an account, returning the new account ID. // Create an account, returning the new account ID.
func (repo *AccountRepositoryMemory) Create(username string, password string, email *string, avatarURL *string) (string, error) { func (repo *AccountRepositoryMemory) Create(username string, password string, email *string, avatarURL *string) (string, error) {
if account, err := repo.GetByUsername(username); err != nil { if account, err := repo.GetByUsername(username); err != nil {
return "", errors.New("Failed to fetch other acccounts by username") return "", errors.NewNotExistError("Failed to fetch other acccounts by username")
} else if account != nil { } else if account != nil {
return "", errors.New("Account with this username already exists") return "", errors.NewNotExistError("Account with this username already exists")
} }
emailRef := "" emailRef := ""
@ -74,9 +76,9 @@ func (repo *AccountRepositoryMemory) Create(username string, password string, em
// more specialised Update* and Remove* functions should be used. // more specialised Update* and Remove* functions should be used.
func (repo *AccountRepositoryMemory) Update(id string, username string, password string, email *string, avatarUrl *string) error { func (repo *AccountRepositoryMemory) Update(id string, username string, password string, email *string, avatarUrl *string) error {
if account, err := repo.GetByUsername(username); err != nil { if account, err := repo.GetByUsername(username); err != nil {
return errors.New("Failed to fetch other acccounts by username") return errors.NewNotExistError("Failed to fetch other acccounts by username")
} else if account != nil && account.ID != id { } else if account != nil && account.ID != id {
return errors.New("Account with this username already exists") return errors.NewNotExistError("Account with this username already exists")
} }
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
@ -93,14 +95,14 @@ func (repo *AccountRepositoryMemory) Update(id string, username string, password
} }
func (repo *AccountRepositoryMemory) UpdateUsername(id string, username string) error { func (repo *AccountRepositoryMemory) UpdateUsername(id string, username string) error {
if account, err := repo.GetByUsername(username); err != nil { if account, err := repo.GetByUsername(username); err != nil {
return errors.New("Failed to fetch other acccounts by username") return errors.NewNotExistError("Failed to fetch other acccounts by username")
} else if account != nil && account.ID != id { } else if account != nil && account.ID != id {
return errors.New("Account with this username already exists") return errors.NewNotExistError("Account with this username already exists")
} }
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
if err != nil { return err } if err != nil { return err }
if account == nil { return errors.New("Account does not exist") } if account == nil { return errors.NewNotExistError("Account does not exist") }
account.Username = username account.Username = username
return nil return nil
@ -108,7 +110,7 @@ func (repo *AccountRepositoryMemory) UpdateUsername(id string, username string)
func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string) error { func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string) error {
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
if err != nil { return err } if err != nil { return err }
if account == nil { return errors.New("Account does not exist") } if account == nil { return errors.NewNotExistError("Account does not exist") }
account.Password = password account.Password = password
return nil return nil
@ -116,7 +118,7 @@ func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string)
func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error { func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error {
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
if err != nil { return err } if err != nil { return err }
if account == nil { return errors.New("Account does not exist") } if account == nil { return errors.NewNotExistError("Account does not exist") }
account.Email.Valid = true account.Email.Valid = true
account.Email.String = email account.Email.String = email
@ -125,7 +127,7 @@ func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error
func (repo *AccountRepositoryMemory) RemoveEmail(id string) error { func (repo *AccountRepositoryMemory) RemoveEmail(id string) error {
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
if err != nil { return err } if err != nil { return err }
if account == nil { return errors.New("Account does not exist") } if account == nil { return errors.NewNotExistError("Account does not exist") }
account.Email.Valid = false account.Email.Valid = false
account.Email.String = "" account.Email.String = ""
@ -134,7 +136,7 @@ func (repo *AccountRepositoryMemory) RemoveEmail(id string) error {
func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string) error { func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string) error {
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
if err != nil { return err } if err != nil { return err }
if account == nil { return errors.New("Account does not exist") } if account == nil { return errors.NewNotExistError("Account does not exist") }
account.AvatarURL.Valid = true account.AvatarURL.Valid = true
account.AvatarURL.String = avatarURL account.AvatarURL.String = avatarURL
@ -143,31 +145,13 @@ func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string
func (repo *AccountRepositoryMemory) RemoveAvatar(id string) error { func (repo *AccountRepositoryMemory) RemoveAvatar(id string) error {
account, err := repo.GetByID(id) account, err := repo.GetByID(id)
if err != nil { return err } if err != nil { return err }
if account == nil { return errors.New("Account does not exist") } if account == nil { return errors.NewNotExistError("Account does not exist") }
account.AvatarURL.Valid = false account.AvatarURL.Valid = false
account.AvatarURL.String = "" account.AvatarURL.String = ""
return nil return nil
} }
func (repo *AccountRepositoryMemory) Delete(id string) error {
accountIndex := -1
for index, account := range repo.accounts {
if account.ID == id {
accountIndex = index
break
}
}
if accountIndex == -1 { return nil }
repo.accounts = append(
repo.accounts[:accountIndex],
repo.accounts[accountIndex+1:]...,
)
return nil
}
// Increment the number of account login failure attempts, // Increment the number of account login failure attempts,
// returning the current fail count. // returning the current fail count.
func (repo *AccountRepositoryMemory) IncrementFails(id string) (int, error) { func (repo *AccountRepositoryMemory) IncrementFails(id string) (int, error) {
@ -188,3 +172,17 @@ func (repo *AccountRepositoryMemory) SetLocked(id string, locked bool) error {
account.Locked = locked account.Locked = locked
return nil return nil
} }
func (repo *AccountRepositoryMemory) Delete(id string) (string, error) {
var deletedID string
newAccounts := []*model.Account{}
for _, account := range repo.accounts {
if account.ID == id {
deletedID = id
continue
}
newAccounts = append(newAccounts, account)
}
repo.accounts = newAccounts
return deletedID, nil
}

View file

@ -157,11 +157,6 @@ func (repo *AccountRepositoryPostgres) RemoveAvatar(id string) error {
return err return err
} }
func (repo *AccountRepositoryPostgres) Delete(id string) error {
_, err := repo.db.Exec("DELETE FROM account WHERE id=$1", id)
return err
}
// Increment the number of account login failure attempts, // Increment the number of account login failure attempts,
// returning the current fail count. // returning the current fail count.
func (repo *AccountRepositoryPostgres) IncrementFails(id string) (int, error) { func (repo *AccountRepositoryPostgres) IncrementFails(id string) (int, error) {
@ -179,3 +174,9 @@ func (repo *AccountRepositoryPostgres) SetLocked(id string, locked bool) error {
_, err := repo.db.Exec("UPDATE account SET locked = $2 WHERE id=$1", id, locked) _, err := repo.db.Exec("UPDATE account SET locked = $2 WHERE id=$1", id, locked)
return err return err
} }
func (repo *AccountRepositoryPostgres) Delete(id string) (string, error) {
var deletedID string
err := repo.db.Get(&deletedID, "DELETE FROM account WHERE id=$1", id)
return deletedID, err
}

View file

@ -0,0 +1,135 @@
package music
import (
"arimelody-web/errors"
"arimelody-web/model"
"slices"
)
func (repo *MusicRepositoryMemory) GetAllArtists() ([]*model.Artist, error) {
return repo.artists, nil
}
func (repo *MusicRepositoryMemory) GetArtistCount() (int, error) {
return len(repo.artists), nil
}
func (repo *MusicRepositoryMemory) GetArtistByID(id string) (*model.Artist, error) {
index := slices.IndexFunc(repo.artists, func(artist *model.Artist) bool {
return artist.ID == id
})
if index == -1 { return nil, nil }
return repo.artists[index], nil
}
func (repo *MusicRepositoryMemory) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
release, err := repo.GetReleaseByID(releaseID)
if err != nil { return nil, err }
artists, err := repo.GetAllArtists()
if err != nil { return nil, err }
artistsNotOnRelease := []*model.Artist{}
for _, artist := range artists {
if !slices.ContainsFunc(release.Credits, func(credit *model.Credit) bool {
return credit.Artist.ID == artist.ID
}) {
artistsNotOnRelease = append(artistsNotOnRelease, artist)
}
}
return artistsNotOnRelease, nil
}
func (repo *MusicRepositoryMemory) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) {
releases, err := repo.GetAllReleases(!showHidden, 0)
if err != nil { return nil, err }
credits := []*model.Credit{}
for _, release := range releases {
credits = append(credits, slices.DeleteFunc(
release.Credits,
func(credit *model.Credit) bool {
return credit.Artist.ID != artistID
},
)...)
}
return credits, nil
}
func (repo *MusicRepositoryMemory) CreateArtist(
id string,
name string,
website string,
avatar string,
) error {
if artist, err := repo.GetArtistByID(id); err != nil {
if !errors.IsNotExistError(err) { return err }
} else {
if artist != nil { return errors.NewValidationError("Artist with this ID already exists") }
}
repo.artists = append(repo.artists, &model.Artist{
ID: id,
Name: name,
Website: website,
Avatar: avatar,
})
return nil
}
func (repo *MusicRepositoryMemory) UpdateArtist(artist *model.Artist) error {
repoArtist, err := repo.GetArtistByID(artist.ID)
if err != nil { return err }
if repoArtist == nil { return errors.NewNotExistError("Artist does not exist") }
repoArtist.Name = artist.Name
repoArtist.Website = artist.Website
repoArtist.Avatar = artist.Avatar
return nil
}
func (repo *MusicRepositoryMemory) UpdateArtistID(oldID string, newID string) error {
artist, err := repo.GetArtistByID(oldID)
if err != nil { return err }
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
artist.ID = newID
return nil
}
func (repo *MusicRepositoryMemory) UpdateArtistName(id string, name string) error {
artist, err := repo.GetArtistByID(id)
if err != nil { return err }
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
artist.Name = name
return nil
}
func (repo *MusicRepositoryMemory) UpdateArtistWebsite(id string, website string) error {
artist, err := repo.GetArtistByID(id)
if err != nil { return err }
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
artist.Website = website
return nil
}
func (repo *MusicRepositoryMemory) UpdateArtistAvatar(id string, avatar string) error {
artist, err := repo.GetArtistByID(id)
if err != nil { return err }
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
artist.Avatar = avatar
return nil
}
func (repo *MusicRepositoryMemory) DeleteArtist(id string) (string, error) {
var deletedID string
newArtists := []*model.Artist{}
for _, artist := range repo.artists {
if artist.ID == id {
deletedID = id
continue
}
newArtists = append(newArtists, artist)
}
repo.artists = newArtists
return deletedID, nil
}

View file

@ -1,17 +1,29 @@
package controller package music
import ( import (
"arimelody-web/model" "arimelody-web/model"
"github.com/jmoiron/sqlx"
) )
// DATABASE func (repo *MusicRepositoryPostgres) GetAllArtists() ([]*model.Artist, error) {
var artists = []*model.Artist{}
func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) { err := repo.db.Select(&artists, "SELECT * FROM artist")
if err != nil {
return nil, err
}
return artists, nil
}
func (repo *MusicRepositoryPostgres) GetArtistCount() (int, error) {
var count int
err := repo.db.Get(&count, "SELECT count(*) FROM artist")
return count, err
}
func (repo *MusicRepositoryPostgres) GetArtistByID(id string) (*model.Artist, error) {
var artist = model.Artist{} var artist = model.Artist{}
err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) err := repo.db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -19,26 +31,10 @@ func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) {
return &artist, nil return &artist, nil
} }
func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) { func (repo *MusicRepositoryPostgres) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
var artists = []*model.Artist{} var artists = []*model.Artist{}
err := db.Select(&artists, "SELECT * FROM artist") err := repo.db.Select(&artists,
if err != nil {
return nil, err
}
return artists, nil
}
func GetArtistCount(db *sqlx.DB) (int, error) {
var count int
err := db.Get(&count, "SELECT count(*) FROM artist")
return count, err
}
func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) {
var artists = []*model.Artist{}
err := db.Select(&artists,
"SELECT * FROM artist "+ "SELECT * FROM artist "+
"WHERE id NOT IN "+ "WHERE id NOT IN "+
"(SELECT artist FROM musiccredit WHERE release=$1)", "(SELECT artist FROM musiccredit WHERE release=$1)",
@ -50,15 +46,15 @@ func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, err
return artists, nil return artists, nil
} }
func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.Credit, error) { func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) {
var query string = "SELECT release.id,title,artwork,release_date,artist.id,name,website,avatar,role,is_primary "+ var query string = "SELECT release.id,title,artwork,release_date,artist.id,name,website,avatar,role,is_primary "+
"FROM musiccredit "+ "FROM musiccredit "+
"JOIN musicrelease AS release ON release=release.id "+ "JOIN musicrelease AS release ON release=release.id "+
"JOIN artist ON artist=artist.id "+ "JOIN artist ON artist=artist.id "+
"WHERE artist=$1 " "WHERE artist=$1 "
if !show_hidden { query += "AND visible=true " } if !showHidden { query += "AND visible=true " }
query += "ORDER BY release_date DESC" query += "ORDER BY release_date DESC"
rows, err := db.Query(query, artistID) rows, err := repo.db.Query(query, artistID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -70,7 +66,10 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.
} }
var credits []*model.Credit var credits []*model.Credit
for rows.Next() { for rows.Next() {
var credit model.Credit credit := &model.Credit{
Release: &model.Release{},
Artist: &model.Artist{},
}
err = rows.Scan( err = rows.Scan(
&credit.Release.ID, &credit.Release.ID,
&credit.Release.Title, &credit.Release.Title,
@ -85,44 +84,42 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.
) )
otherArtists := []NamePrimary{} otherArtists := []NamePrimary{}
err = db.Select(&otherArtists, err = repo.db.Select(&otherArtists,
"SELECT name,is_primary FROM artist "+ "SELECT name,is_primary FROM artist "+
"JOIN musiccredit ON artist=id "+ "JOIN musiccredit ON artist=id "+
"WHERE release=$1", "WHERE release=$1",
credit.Release.ID) credit.Release.ID)
for _, otherCredit := range otherArtists { for _, otherCredit := range otherArtists {
credit.Release.Credits = append(credit.Release.Credits, &model.Credit{ credit.Release.Credits = append(credit.Release.Credits, &model.Credit{
Artist: model.Artist{ Artist: &model.Artist{
Name: otherCredit.Name, Name: otherCredit.Name,
}, },
Primary: otherCredit.Primary, Primary: otherCredit.Primary,
}) })
} }
credits = append(credits, &credit) credits = append(credits, credit)
} }
return credits, nil return credits, nil
} }
func CreateArtist(db *sqlx.DB, artist *model.Artist) error { func (repo *MusicRepositoryPostgres) CreateArtist(
_, err := db.Exec( id string,
name string,
website string,
avatar string,
) error {
_, err := repo.db.Exec(
"INSERT INTO artist (id, name, website, avatar) "+ "INSERT INTO artist (id, name, website, avatar) "+
"VALUES ($1, $2, $3, $4)", "VALUES ($1, $2, $3, $4)",
artist.ID, id, name, website, avatar,
artist.Name,
artist.Website,
artist.Avatar,
) )
if err != nil {
return err return err
}
return nil
} }
func UpdateArtist(db *sqlx.DB, artist *model.Artist) error { func (repo *MusicRepositoryPostgres) UpdateArtist(artist *model.Artist) error {
_, err := db.Exec( _, err := repo.db.Exec(
"UPDATE artist "+ "UPDATE artist "+
"SET name=$2, website=$3, avatar=$4 "+ "SET name=$2, website=$3, avatar=$4 "+
"WHERE id=$1", "WHERE id=$1",
@ -131,22 +128,27 @@ func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
artist.Website, artist.Website,
artist.Avatar, artist.Avatar,
) )
if err != nil {
return err return err
} }
func (repo *MusicRepositoryPostgres) UpdateArtistID(oldID string, newID string) error {
return nil _, err := repo.db.Exec("UPDATE artist SET id=$2 WHERE id=$1", oldID, newID)
return err
}
func (repo *MusicRepositoryPostgres) UpdateArtistName(id string, name string) error {
_, err := repo.db.Exec("UPDATE artist SET name=$2 WHERE id=$1", id, name)
return err
}
func (repo *MusicRepositoryPostgres) UpdateArtistWebsite(id string, website string) error {
_, err := repo.db.Exec("UPDATE artist SET website=$2 WHERE id=$1", id, website)
return err
}
func (repo *MusicRepositoryPostgres) UpdateArtistAvatar(id string, avatar string) error {
_, err := repo.db.Exec("UPDATE artist SET avatar=$2 WHERE id=$1", id, avatar)
return err
} }
func DeleteArtist(db *sqlx.DB, artistID string) error { func (repo *MusicRepositoryPostgres) DeleteArtist(id string) (string, error) {
_, err := db.Exec( var deletedID string
"DELETE FROM artist "+ err := repo.db.Get(&deletedID, "DELETE FROM artist WHERE id=$1", id)
"WHERE id=$1", return deletedID, err
artistID,
)
if err != nil {
return err
}
return nil
} }

View file

@ -0,0 +1,110 @@
package music
import (
"arimelody-web/model"
"time"
"github.com/jmoiron/sqlx"
)
type MusicRepository interface {
// artists
GetAllArtists() ([]*model.Artist, error)
GetArtistCount() (int, error)
// Fetches an artist by ID, returning an error if one was encountered.
// If the artist does not exist, both response fields are nil.
GetArtistByID(id string) (*model.Artist, error)
GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error)
GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error)
CreateArtist(id string, name string, website string, avatar string) error
UpdateArtist(artist *model.Artist) error
UpdateArtistID(oldID string, newID string) error
UpdateArtistName(id string, name string) error
UpdateArtistWebsite(id string, website string) error
UpdateArtistAvatar(id string, avatar string) error
DeleteArtist(id string) (string, error)
// releases
// Fetch all releases.
// Filters to visible releases if `onlyVisible = true`.
// If `limit > 0`, limits the number of results.
GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error)
GetReleaseCount(onlyVisible bool) (int, error)
GetReleaseByID(id string) (*model.Release, error)
GetReleaseTracks(id string) ([]*model.Track, error)
GetReleaseCredits(id string) ([]*model.Credit, error)
GetReleaseLinks(id string) ([]*model.Link, error)
CreateRelease(id string, title string, releaseType model.ReleaseType, releaseDate time.Time, artworkURL string) error
UpdateRelease(release *model.Release) error
UpdateReleaseID(oldID string, newID string) error
UpdateReleaseVisibility(id string, visible bool) error
UpdateReleaseTitle(id string, title string) error
UpdateReleaseDescription(id string, description string) error
UpdateReleaseType(id string, releaseType model.ReleaseType) error
UpdateReleaseDate(id string, releaseDate time.Time) error
UpdateReleaseArtwork(id string, artwork string) error
UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error
UpdateReleaseCopyright(id string, copyright string, url string) error
UpdateReleaseTracks(id string, newTrackIDs []string) error
UpdateReleaseCredits(id string, newCredits []*model.Credit) error
UpdateReleaseLinks(id string, newLinks []*model.Link) error
DeleteRelease(id string) (string, error)
// tracks
GetAllTracks() ([]*model.Track, error)
GetTrackCount() (int, error)
GetTrackByID(id string) (*model.Track, error)
GetOrphanTracks() ([]*model.Track, error)
GetTracksNotOnRelease(releaseID string) ([]*model.Track, error)
GetTrackReleases(trackID string) ([]*model.Release, error)
CreateTrack(title string, description string, lyrics string, previewURL string) (string, error)
UpdateTrack(track *model.Track) error
UpdateTrackTitle(id string, title string) error
UpdateTrackDescription(id string, description string) error
UpdateTrackLyrics(id string, lyrics string) error
UpdateTrackPreviewURL(id string, previewURL string) error
DeleteTrack(id string) (string, error)
}
type (
MusicRepositoryPostgres struct {
db *sqlx.DB
}
MusicRepositoryMemory struct {
artists []*model.Artist
releases []*model.Release
tracks []*model.Track
}
)
var _ MusicRepository = new(MusicRepositoryPostgres)
func NewMusicRepositoryPostgres(db *sqlx.DB) *MusicRepositoryPostgres {
return &MusicRepositoryPostgres{ db: db }
}
var _ MusicRepository = new(MusicRepositoryMemory)
func NewMusicRepositoryMemory(
artists []*model.Artist,
releases []*model.Release,
tracks []*model.Track,
) *MusicRepositoryMemory {
return &MusicRepositoryMemory{
artists: artists,
releases: releases,
tracks: tracks,
}
}

View file

@ -0,0 +1,228 @@
package music
import (
"arimelody-web/errors"
"arimelody-web/model"
"fmt"
"slices"
"time"
)
func (repo *MusicRepositoryMemory) GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
releases := []*model.Release{}
for _, release := range repo.releases {
if !onlyVisible || release.Visible {
releases = append(releases, release)
}
}
return releases, nil
}
func (repo *MusicRepositoryMemory) GetReleaseCount(onlyVisible bool) (int, error) {
releaseCount := 0
for _, release := range repo.releases {
if !onlyVisible || release.Visible {
releaseCount++
}
}
return releaseCount, nil
}
func (repo *MusicRepositoryMemory) GetReleaseByID(id string) (*model.Release, error) {
index := slices.IndexFunc(repo.releases, func(release *model.Release) bool {
return release.ID == id
})
if index == -1 { return nil, nil }
return repo.releases[index], nil
}
func (repo *MusicRepositoryMemory) GetReleaseTracks(id string) ([]*model.Track, error) {
release, err := repo.GetReleaseByID(id)
if err != nil { return nil, err }
return release.Tracks, nil
}
func (repo *MusicRepositoryMemory) GetReleaseCredits(id string) ([]*model.Credit, error) {
release, err := repo.GetReleaseByID(id)
if err != nil { return nil, err }
return release.Credits, nil
}
func (repo *MusicRepositoryMemory) GetReleaseLinks(id string) ([]*model.Link, error) {
release, err := repo.GetReleaseByID(id)
if err != nil { return nil, err }
return release.Links, nil
}
func (repo *MusicRepositoryMemory) CreateRelease(
id string,
title string,
releaseType model.ReleaseType,
releaseDate time.Time,
artworkURL string,
) error {
if release, err := repo.GetReleaseByID(id); err != nil {
if !errors.IsNotExistError(err) { return err }
} else {
if release != nil { return errors.NewValidationError("Release with this ID already exists") }
}
repo.releases = append(repo.releases, &model.Release{
ID: id,
Title: title,
ReleaseType: releaseType,
ReleaseDate: releaseDate,
Artwork: artworkURL,
})
return nil
}
func (repo *MusicRepositoryMemory) UpdateRelease(release *model.Release) error {
repoRelease, err := repo.GetReleaseByID(release.ID)
if err != nil { return err }
if repoRelease == nil { return errors.NewNotExistError("Release does not exist") }
repoRelease.Visible = release.Visible
repoRelease.Title = release.Title
repoRelease.Description = release.Description
repoRelease.ReleaseType = release.ReleaseType
repoRelease.ReleaseDate = release.ReleaseDate
repoRelease.Artwork = release.Artwork
repoRelease.Buyname = release.Buyname
repoRelease.Buylink = release.Buylink
repoRelease.Copyright = release.Copyright
repoRelease.CopyrightURL = release.CopyrightURL
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseID(oldID string, newID string) error {
release, err := repo.GetReleaseByID(oldID)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.ID = newID
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseVisibility(id string, visible bool) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Visible = visible
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseTitle(id string, title string) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Title = title
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseDescription(id string, description string) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Description = description
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseType(id string, releaseType model.ReleaseType) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.ReleaseType = releaseType
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseDate(id string, releaseDate time.Time) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.ReleaseDate = releaseDate
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseArtwork(id string, artwork string) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Artwork = artwork
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Buyname = buyName
release.Buylink = buyLink
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseCopyright(id string, copyright string, url string) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Copyright = copyright
release.CopyrightURL = url
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseTracks(id string, newTrackIDs []string) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
tracks := []*model.Track{}
for _, trackID := range newTrackIDs {
track, err := repo.GetTrackByID(trackID)
if err != nil {
if errors.IsNotExistError(err) {
return errors.NewNotExistError(fmt.Sprintf("Track %s does not exist", trackID))
}
return err
}
tracks = append(tracks, track)
}
release.Tracks = tracks
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseCredits(id string, newCredits []*model.Credit) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
for _, credit := range newCredits {
if credit.Artist == nil { return errors.NewValidationError("Credit artist cannot be empty") }
if len(credit.Artist.ID) == 0 { return errors.NewValidationError("Credit artist ID cannot be empty") }
if artist, err := repo.GetArtistByID(credit.Artist.ID); err != nil {
return err
} else if artist == nil {
return errors.NewNotExistError(fmt.Sprintf("Artist '%s' does not exist", credit.Artist.ID))
} else {
credit.Release = release
credit.Artist = artist
}
}
release.Credits = newCredits
return nil
}
func (repo *MusicRepositoryMemory) UpdateReleaseLinks(id string, newLinks []*model.Link) error {
release, err := repo.GetReleaseByID(id)
if err != nil { return err }
if release == nil { return errors.NewNotExistError("Release does not exist") }
release.Links = newLinks
return nil
}
func (repo *MusicRepositoryMemory) DeleteRelease(id string) (string, error) {
var deletedID string
newReleases := []*model.Release{}
for _, release := range repo.releases {
if release.ID == id {
deletedID = id
continue
}
newReleases = append(newReleases, release)
}
repo.releases = newReleases
return deletedID, nil
}

View file

@ -0,0 +1,303 @@
package music
import (
"arimelody-web/model"
"time"
)
func (repo *MusicRepositoryPostgres) GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
var releases = []*model.Release{}
query := "SELECT * FROM musicrelease"
if onlyVisible {
query += " WHERE visible=true"
}
query += " ORDER BY release_date DESC"
var err error
if limit > 0 {
err = repo.db.Select(&releases, query + " LIMIT $1", limit)
} else {
err = repo.db.Select(&releases, query)
}
if err != nil {
return nil, err
}
return releases, nil
}
func (repo *MusicRepositoryPostgres) GetReleaseCount(onlyVisible bool) (int, error) {
query := "SELECT count(*) FROM musicrelease"
if onlyVisible {
query += " WHERE visible=true"
}
var count int
err := repo.db.Get(&count, query)
return count, err
}
func (repo *MusicRepositoryPostgres) GetReleaseByID(id string) (*model.Release, error) {
var release = model.Release{}
err := repo.db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
if err != nil { return nil, err }
return &release, nil
}
func (repo *MusicRepositoryPostgres) GetReleaseTracks(releaseID string) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := repo.db.Select(&tracks,
"SELECT musictrack.* FROM musictrack "+
"JOIN musicreleasetrack ON track=id "+
"WHERE release=$1 "+
"ORDER BY number ASC",
releaseID,
)
if err != nil {
return nil, err
}
return tracks, nil
}
func (repo *MusicRepositoryPostgres) GetReleaseCredits(releaseID string) ([]*model.Credit, error) {
rows, err := repo.db.Query(
"SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
"FROM musiccredit "+
"JOIN artist ON artist=artist.id "+
"JOIN musicrelease ON release=musicrelease.id "+
"WHERE musicrelease.id=$1 "+
"ORDER BY is_primary DESC",
releaseID,
)
if err != nil {
return nil, err
}
var credits []*model.Credit
for rows.Next() {
credit := &model.Credit{
Artist: &model.Artist{},
}
rows.Scan(
&credit.Artist.ID,
&credit.Artist.Name,
&credit.Artist.Website,
&credit.Artist.Avatar,
&credit.Role,
&credit.Primary)
credits = append(credits, credit)
}
return credits, nil
}
func (repo *MusicRepositoryPostgres) GetReleaseLinks(releaseID string) ([]*model.Link, error) {
var links = []*model.Link{}
err := repo.db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID)
if err != nil {
return nil, err
}
return links, nil
}
func (repo *MusicRepositoryPostgres) CreateRelease(
id string,
title string,
releaseType model.ReleaseType,
releaseDate time.Time,
artworkURL string,
) error {
_, err := repo.db.Exec(
"INSERT INTO musicrelease "+
"(id, title, type, release_date, artwork) "+
"VALUES ($1, $2, $3, $4, $5)",
id,
title,
releaseType,
releaseDate.Format("2006-01-02 15:04:05"),
artworkURL,
)
if err != nil {
return err
}
return nil
}
func (repo *MusicRepositoryPostgres) UpdateRelease(release *model.Release) error {
_, err := repo.db.Exec(
"UPDATE musicrelease SET "+
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+
"WHERE id=$1",
release.ID,
release.Visible,
release.Title,
release.Description,
release.ReleaseType,
release.ReleaseDate.Format("2006-01-02 15:04:05"),
release.Artwork,
release.Buyname,
release.Buylink,
release.Copyright,
release.CopyrightURL,
)
if err != nil {
return err
}
return nil
}
func (repo *MusicRepositoryPostgres) UpdateReleaseID(oldID string, newID string) error {
_, err := repo.db.Exec("UPDATE musicrelease SET id=$2 WHERE id=$1", oldID, newID)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseVisibility(id string, visible bool) error {
_, err := repo.db.Exec("UPDATE musicrelease SET visible=$2 WHERE id=$1", id, visible)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseTitle(id string, title string) error {
_, err := repo.db.Exec("UPDATE musicrelease SET title=$2 WHERE id=$1", id, title)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseDescription(id string, description string) error {
_, err := repo.db.Exec("UPDATE musicrelease SET description=$2 WHERE id=$1", id, description)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseType(id string, releaseType model.ReleaseType) error {
_, err := repo.db.Exec("UPDATE musicrelease SET type=$2 WHERE id=$1", id, releaseType)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseDate(id string, releaseDate time.Time) error {
_, err := repo.db.Exec(
"UPDATE musicrelease SET release_date=$2 WHERE id=$1",
id,
releaseDate.Format("2006-01-02 15:04:05"),
)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseArtwork(id string, artwork string) error {
_, err := repo.db.Exec("UPDATE musicrelease SET artwork=$2 WHERE id=$1", id, artwork)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error {
_, err := repo.db.Exec(
"UPDATE musicrelease SET buyname=$2,buylink=$3 WHERE id=$1",
id, buyName, buyLink,
)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseCopyright(id string, copyright string, url string) error {
_, err := repo.db.Exec(
"UPDATE musicrelease SET copyright=$2,copyrighturl=$3 WHERE id=$1",
id, copyright, url,
)
return err
}
func (repo *MusicRepositoryPostgres) UpdateReleaseTracks(releaseID string, newTrackIDs []string) error {
tx, err := repo.db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
if err != nil {
return err
}
for i, trackID := range newTrackIDs {
_, err = tx.Exec(
"INSERT INTO musicreleasetrack "+
"(release, track, number) "+
"VALUES ($1, $2, $3)",
releaseID,
trackID,
i)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (repo *MusicRepositoryPostgres) UpdateReleaseCredits(releaseID string, newCredits []*model.Credit) error {
tx, err := repo.db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
if err != nil {
return err
}
for _, credit := range newCredits {
_, err = tx.Exec(
"INSERT INTO musiccredit "+
"(release, artist, role, is_primary) "+
"VALUES ($1, $2, $3, $4)",
releaseID,
credit.Artist.ID,
credit.Role,
credit.Primary,
)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (repo *MusicRepositoryPostgres) UpdateReleaseLinks(releaseID string, newLinks []*model.Link) error {
tx, err := repo.db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
if err != nil {
return err
}
for _, link := range newLinks {
_, err := tx.Exec(
"INSERT INTO musiclink "+
"(release, name, url) "+
"VALUES ($1, $2, $3)",
releaseID,
link.Name,
link.URL,
)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (repo *MusicRepositoryPostgres) DeleteRelease(id string) (string, error) {
var deletedID string
err := repo.db.Get(&deletedID, "DELETE FROM musicrelease WHERE id=$1", id)
return deletedID, err
}

View file

@ -0,0 +1,123 @@
package music
import (
"arimelody-web/errors"
"arimelody-web/model"
"slices"
"strconv"
)
func (repo *MusicRepositoryMemory) GetAllTracks() ([]*model.Track, error) {
return repo.tracks, nil
}
func (repo *MusicRepositoryMemory) GetTrackCount() (int, error) {
return len(repo.tracks), nil
}
func (repo *MusicRepositoryMemory) GetTrackByID(id string) (*model.Track, error) {
index := slices.IndexFunc(repo.tracks, func(track *model.Track) bool {
return track.ID == id
})
if index == -1 { return nil, nil }
return repo.tracks[index], nil
}
func (repo *MusicRepositoryMemory) GetOrphanTracks() ([]*model.Track, error) {
return slices.DeleteFunc(repo.tracks, func(track *model.Track) bool {
return slices.ContainsFunc(repo.releases, func(release *model.Release) bool {
return slices.ContainsFunc(release.Tracks, func(releaseTrack *model.Track) bool {
return releaseTrack.ID == track.ID
})
})
}), nil
}
func (repo *MusicRepositoryMemory) GetTracksNotOnRelease(releaseID string) ([]*model.Track, error) {
release, err := repo.GetReleaseByID(releaseID)
if err != nil { return nil, err }
return slices.DeleteFunc(repo.tracks, func(track *model.Track) bool {
return slices.ContainsFunc(release.Tracks, func(releaseTrack *model.Track) bool {
return releaseTrack.ID == track.ID
})
}), nil
}
func (repo *MusicRepositoryMemory) GetTrackReleases(trackID string) ([]*model.Release, error) {
return slices.DeleteFunc(repo.releases, func(release *model.Release) bool {
return !slices.ContainsFunc(release.Tracks, func(track *model.Track) bool {
return track.ID == trackID
})
}), nil
}
func (repo *MusicRepositoryMemory) CreateTrack(
title string,
description string,
lyrics string,
previewURL string,
) (string, error) {
id := strconv.Itoa(len(repo.tracks))
repo.tracks = append(repo.tracks, &model.Track{
ID: id,
Title: title,
Description: description,
Lyrics: lyrics,
PreviewURL: previewURL,
})
return id, nil
}
func (repo *MusicRepositoryMemory) UpdateTrack(track *model.Track) error {
repoTrack, err := repo.GetTrackByID(track.ID)
if err != nil { return err }
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
repoTrack.Title = track.Title
repoTrack.Description = track.Description
repoTrack.Lyrics = track.Lyrics
repoTrack.PreviewURL = track.PreviewURL
return nil
}
func (repo *MusicRepositoryMemory) UpdateTrackTitle(id string, title string) error {
repoTrack, err := repo.GetTrackByID(id)
if err != nil { return err }
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
repoTrack.Title = title
return nil
}
func (repo *MusicRepositoryMemory) UpdateTrackDescription(id string, description string) error {
repoTrack, err := repo.GetTrackByID(id)
if err != nil { return err }
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
repoTrack.Description = description
return nil
}
func (repo *MusicRepositoryMemory) UpdateTrackLyrics(id string, lyrics string) error {
repoTrack, err := repo.GetTrackByID(id)
if err != nil { return err }
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
repoTrack.Lyrics = lyrics
return nil
}
func (repo *MusicRepositoryMemory) UpdateTrackPreviewURL(id string, previewURL string) error {
repoTrack, err := repo.GetTrackByID(id)
if err != nil { return err }
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
repoTrack.PreviewURL = previewURL
return nil
}
func (repo *MusicRepositoryMemory) DeleteTrack(id string) (string, error) {
var deletedID string
newTracks := []*model.Track{}
for _, track := range repo.tracks {
if track.ID == id {
deletedID = id
continue
}
newTracks = append(newTracks, track)
}
repo.tracks = newTracks
return deletedID, nil
}

View file

@ -0,0 +1,137 @@
package music
import (
"arimelody-web/model"
)
func (repo *MusicRepositoryPostgres) GetAllTracks() ([]*model.Track, error) {
var tracks = []*model.Track{}
err := repo.db.Select(&tracks, "SELECT * FROM musictrack")
if err != nil {
return nil, err
}
return tracks, nil
}
func (repo *MusicRepositoryPostgres) GetTrackCount() (int, error) {
var count int
err := repo.db.Get(&count, "SELECT count(*) FROM musictrack")
return count, err
}
func (repo *MusicRepositoryPostgres) GetTrackByID(id string) (*model.Track, error) {
var track = model.Track{}
stmt, _ := repo.db.Preparex("SELECT * FROM musictrack WHERE id=$1")
err := stmt.Get(&track, id)
if err != nil {
return nil, err
}
return &track, nil
}
func (repo *MusicRepositoryPostgres) GetOrphanTracks() ([]*model.Track, error) {
var tracks = []*model.Track{}
err := repo.db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)")
if err != nil {
return nil, err
}
return tracks, nil
}
func (repo *MusicRepositoryPostgres) GetTracksNotOnRelease(releaseID string) ([]*model.Track, error) {
var tracks = []*model.Track{}
err := repo.db.Select(&tracks,
"SELECT * FROM musictrack "+
"WHERE id NOT IN "+
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
releaseID)
if err != nil {
return nil, err
}
return tracks, nil
}
func (repo *MusicRepositoryPostgres) GetTrackReleases(trackID string) ([]*model.Release, error) {
var releases = []*model.Release{}
err := repo.db.Select(&releases,
"SELECT id,title,type,release_date,artwork,buylink "+
"FROM musicrelease "+
"JOIN musicreleasetrack ON release=id "+
"WHERE track=$1 "+
"ORDER BY release_date",
trackID,
)
if err != nil { return nil, err }
return releases, nil
}
func (repo *MusicRepositoryPostgres) CreateTrack(
title string,
description string,
lyrics string,
previewURL string,
) (string, error) {
var trackID string
err := repo.db.QueryRow(
"INSERT INTO musictrack (title, description, lyrics, preview_url) "+
"VALUES ($1, $2, $3, $4) "+
"RETURNING id",
title,
description,
lyrics,
previewURL,
).Scan(&trackID)
if err != nil {
return "", err
}
return trackID, nil
}
func (repo *MusicRepositoryPostgres) UpdateTrack(track *model.Track) error {
_, err := repo.db.Exec(
"UPDATE musictrack "+
"SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+
"WHERE id=$1",
track.ID,
track.Title,
track.Description,
track.Lyrics,
track.PreviewURL,
)
if err != nil {
return err
}
return nil
}
func (repo *MusicRepositoryPostgres) UpdateTrackTitle(id string, title string) error {
_, err := repo.db.Exec("UPDATE musictrack SET title=$2 WHERE id=$1", id, title)
return err
}
func (repo *MusicRepositoryPostgres) UpdateTrackDescription(id string, description string) error {
_, err := repo.db.Exec("UPDATE musictrack SET description=$2 WHERE id=$1", id, description)
return err
}
func (repo *MusicRepositoryPostgres) UpdateTrackLyrics(id string, lyrics string) error {
_, err := repo.db.Exec("UPDATE musictrack SET lyrics=$2 WHERE id=$1", id, lyrics)
return err
}
func (repo *MusicRepositoryPostgres) UpdateTrackPreviewURL(id string, previewURL string) error {
_, err := repo.db.Exec("UPDATE musictrack SET preview_url=$2 WHERE id=$1", id, previewURL)
return err
}
func (repo *MusicRepositoryPostgres) DeleteTrack(id string) (string, error) {
var deletedID string
err := repo.db.Get(&deletedID, "DELETE FROM musictrack WHERE id=$1 RETURNING id", id)
return deletedID, err
}

View file

@ -1,10 +1,10 @@
package account package account
import ( import (
"arimelody-web/errors"
"arimelody-web/model" "arimelody-web/model"
repository "arimelody-web/repository/account" repository "arimelody-web/repository/account"
"errors" "arimelody-web/service/validator"
"fmt"
"log" "log"
) )
@ -29,33 +29,24 @@ func (s *AccountService) GetCount() (int, error) {
} }
func (s *AccountService) GetByID(id string) (*model.Account, error) { func (s *AccountService) GetByID(id string) (*model.Account, error) {
if account, err := s.repo.GetByID(id); err != nil { account, err := s.repo.GetByID(id)
return nil, err if err != nil { return nil, err }
} else if account == nil { if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
return nil, fmt.Errorf("Account does not exist: %s", id)
} else {
return account, nil return account, nil
}
} }
func (s *AccountService) GetByUsername(username string) (*model.Account, error) { func (s *AccountService) GetByUsername(username string) (*model.Account, error) {
if account, err := s.repo.GetByUsername(username); err != nil { account, err := s.repo.GetByUsername(username)
return nil, err if err != nil { return nil, err }
} else if account == nil { if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
return nil, fmt.Errorf("Account does not exist: %s", username)
} else {
return account, nil return account, nil
}
} }
func (s *AccountService) GetByEmail(email string) (*model.Account, error) { func (s *AccountService) GetByEmail(email string) (*model.Account, error) {
if account, err := s.repo.GetByEmail(email); err != nil { account, err := s.repo.GetByEmail(email)
return nil, err if err != nil { return nil, err }
} else if account == nil { if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
return nil, fmt.Errorf("Account does not exist with email: %s", email)
} else {
return account, nil return account, nil
}
} }
func (s *AccountService) Create( func (s *AccountService) Create(
@ -64,13 +55,13 @@ func (s *AccountService) Create(
email *string, email *string,
avatarURL *string, avatarURL *string,
) (string, error) { ) (string, error) {
if len(username) == 0 { return "", errors.NewValidationError("Username cannot be empty") }
if !validator.ValidateID(username) { return "", errors.NewValidationError("Username contains invalid characters") }
if len(password) == 0 { return "", errors.NewValidationError("Password cannot be empty") }
if email != nil && len(*email) == 0 { return "", errors.NewValidationError("Email cannot be empty") }
var id string var id string
var err error var err error
if len(username) == 0 { return id, errors.New("Username cannot be empty") }
if len(password) == 0 { return id, errors.New("Password cannot be empty") }
if email != nil && len(*email) == 0 { return id, errors.New("Email cannot be empty") }
if id, err = s.repo.Create(username, password, email, avatarURL); err != nil { if id, err = s.repo.Create(username, password, email, avatarURL); err != nil {
return id, err return id, err
} }
@ -80,48 +71,31 @@ func (s *AccountService) Create(
} }
func (s *AccountService) ChangeUsername(id string, username string) error { func (s *AccountService) ChangeUsername(id string, username string) error {
if len(username) == 0 { return errors.New("Username cannot be empty") } if len(username) == 0 { return errors.NewValidationError("Username cannot be empty") }
if err := s.repo.UpdateAvatarURL(id, username); err != nil { if !validator.ValidateID(username) { return errors.NewValidationError("Username contains invalid characters") }
return err if err := s.repo.UpdateUsername(id, username); err != nil { return err }
}
s.log.Printf("Changed username for %s to '%s'", id, username) s.log.Printf("Changed username for %s to '%s'", id, username)
return nil return nil
} }
func (s *AccountService) ChangePassword(id string, password string) error { func (s *AccountService) ChangePassword(id string, password string) error {
if len(password) == 0 { return errors.New("Password cannot be empty") } if len(password) == 0 { return errors.NewValidationError("Password cannot be empty") }
if err := s.repo.UpdateAvatarURL(id, password); err != nil { if err := s.repo.UpdatePassword(id, password); err != nil { return err }
return err
}
s.log.Printf("Changed password for %s", id) s.log.Printf("Changed password for %s", id)
return nil return nil
} }
func (s *AccountService) ChangeEmail(id string, email string) error { func (s *AccountService) ChangeEmail(id string, email string) error {
if len(email) == 0 { return s.repo.RemoveEmail(id) } if len(email) == 0 { return s.repo.RemoveEmail(id) }
if err := s.repo.UpdateAvatarURL(id, email); err != nil { if err := s.repo.UpdateEmail(id, email); err != nil { return err }
return err
}
s.log.Printf("Changed email for %s to '%s'", id, email) s.log.Printf("Changed email for %s to '%s'", id, email)
return nil return nil
} }
func (s *AccountService) ChangeAvatarURL(id string, avatarURL string) error { func (s *AccountService) ChangeAvatarURL(id string, avatarURL string) error {
if len(avatarURL) == 0 { return s.repo.RemoveAvatar(id) } if len(avatarURL) == 0 { return s.repo.RemoveAvatar(id) }
if err := s.repo.UpdateAvatarURL(id, avatarURL); err != nil { if err := s.repo.UpdateAvatarURL(id, avatarURL); err != nil { return err }
return err
}
s.log.Printf("Changed avatar URL for %s to '%s'", id, avatarURL) s.log.Printf("Changed avatar URL for %s to '%s'", id, avatarURL)
return nil return nil
} }
func (s *AccountService) Delete(id string) error {
if err := s.repo.Delete(id); err != nil { return err }
s.log.Printf("Deleted account %s", id)
return nil
}
func (s *AccountService) IncrementFails(id string) (int, error) { func (s *AccountService) IncrementFails(id string) (int, error) {
num, err := s.repo.IncrementFails(id) num, err := s.repo.IncrementFails(id)
if err != nil { return 0, err } if err != nil { return 0, err }
@ -146,3 +120,11 @@ func (s *AccountService) Unlock(id string) error {
s.log.Printf("Unlocked account %s", id) s.log.Printf("Unlocked account %s", id)
return nil return nil
} }
func (s *AccountService) Delete(id string) error {
deletedID, err := s.repo.Delete(id)
if err != nil { return err }
if deletedID == "" { return errors.NewNotExistError("Account does not exist") }
s.log.Printf("Deleted account %s", id)
return nil
}

View file

@ -1,16 +1,19 @@
package account package account_test
import ( import (
"arimelody-web/model" "arimelody-web/model"
repository "arimelody-web/repository/account" repository "arimelody-web/repository/account"
service "arimelody-web/service/account"
"arimelody-web/errors"
"log" "log"
"os" "os"
"testing" "testing"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
var ( var (
service *AccountService s *service.AccountService
) )
func init() { func init() {
@ -19,7 +22,7 @@ func init() {
defer devNullFile.Close() defer devNullFile.Close()
repo := repository.NewAccountRepositoryMemory(make([]*model.Account, 0)) repo := repository.NewAccountRepositoryMemory(make([]*model.Account, 0))
service = NewAccountService( s = service.NewAccountService(
repo, repo,
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS), log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
) )
@ -41,14 +44,14 @@ func Test_Account(t *testing.T) {
username := "testificate" username := "testificate"
password := "the amazing digital data breach" password := "the amazing digital data breach"
email := "goober@arimelody.space" email := "goober@arimelody.space"
avatarURL := "/img/default-avatar.webp" avatarURL := "/img/account-avatar.webp"
var id string var id string
var err error var err error
t.Run("accounts should start empty", func(t *testing.T) { t.Run("accounts should start empty", func(t *testing.T) {
t.Run("count is zero", func(t *testing.T) { t.Run("count is zero", func(t *testing.T) {
if num, err := service.GetCount(); err != nil { if num, err := s.GetCount(); err != nil {
t.Errorf("Failed to get number of accounts: %v", err) t.Errorf("Failed to get number of accounts: %v", err)
} else { } else {
assert.Equal(t, num, 0) assert.Equal(t, num, 0)
@ -56,8 +59,8 @@ func Test_Account(t *testing.T) {
}) })
t.Run("service returns empty array", func(t *testing.T) { t.Run("service returns empty array", func(t *testing.T) {
if accounts, err := service.GetAll(); err != nil { if accounts, err := s.GetAll(); err != nil {
t.Errorf("Failed to get number of accounts: %v", err) t.Errorf("Failed to get accounts: %v", err)
} else { } else {
assert.Equal(t, len(accounts), 0) assert.Equal(t, len(accounts), 0)
} }
@ -65,13 +68,28 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can create account", func(t *testing.T) { t.Run("can create account", func(t *testing.T) {
id, err = service.Create(username, password, &email, &avatarURL) id, err = s.Create(username, password, &email, &avatarURL)
if err != nil { if err != nil {
t.Errorf("Failed to create account: %v", err) t.Errorf("Failed to create account: %v", err)
} }
t.Run("but not with invalid username", func(t *testing.T) {
if _, err := s.Create("", password, &email, &avatarURL); err == nil {
t.Error("Could create account with invalid username")
} else if !errors.IsValidationError(err) {
t.Error("Error is not validation error")
}
})
t.Run("but not with invalid password", func(t *testing.T) {
if _, err := s.Create("test-username", "", &email, &avatarURL); err == nil {
t.Error("Could create account with invalid password")
} else if !errors.IsValidationError(err) {
t.Error("Error is not validation error")
}
})
t.Run("and fetch by ID", func(t *testing.T) { t.Run("and fetch by ID", func(t *testing.T) {
account, err := service.GetByID(id) account, err := s.GetByID(id)
if err != nil { if err != nil {
t.Errorf("Failed to get account after creation: %v", err) t.Errorf("Failed to get account after creation: %v", err)
} }
@ -84,7 +102,7 @@ func Test_Account(t *testing.T) {
assert.Equal(t, account.Locked, false) assert.Equal(t, account.Locked, false)
}) })
t.Run("and fetch by username", func(t *testing.T) { t.Run("and fetch by username", func(t *testing.T) {
account, err := service.GetByUsername(username) account, err := s.GetByUsername(username)
if err != nil { if err != nil {
t.Errorf("Failed to get account after creation: %v", err) t.Errorf("Failed to get account after creation: %v", err)
} }
@ -97,7 +115,7 @@ func Test_Account(t *testing.T) {
assert.Equal(t, account.Locked, false) assert.Equal(t, account.Locked, false)
}) })
t.Run("and fetch by email", func(t *testing.T) { t.Run("and fetch by email", func(t *testing.T) {
account, err := service.GetByEmail(email) account, err := s.GetByEmail(email)
if err != nil { if err != nil {
t.Errorf("Failed to get account after creation: %v", err) t.Errorf("Failed to get account after creation: %v", err)
} }
@ -113,7 +131,7 @@ func Test_Account(t *testing.T) {
t.Run("number of accounts should increment", func(t *testing.T) { t.Run("number of accounts should increment", func(t *testing.T) {
t.Run("count is one", func(t *testing.T) { t.Run("count is one", func(t *testing.T) {
if num, err := service.GetCount(); err != nil { if num, err := s.GetCount(); err != nil {
t.Errorf("Failed to get number of accounts: %v", err) t.Errorf("Failed to get number of accounts: %v", err)
} else { } else {
assert.Equal(t, num, 1) assert.Equal(t, num, 1)
@ -121,8 +139,8 @@ func Test_Account(t *testing.T) {
}) })
t.Run("service returns array with one account", func(t *testing.T) { t.Run("service returns array with one account", func(t *testing.T) {
if accounts, err := service.GetAll(); err != nil { if accounts, err := s.GetAll(); err != nil {
t.Errorf("Failed to get number of accounts: %v", err) t.Errorf("Failed to get accounts: %v", err)
} else { } else {
assert.Equal(t, len(accounts), 1) assert.Equal(t, len(accounts), 1)
} }
@ -130,7 +148,7 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can't create duplicate account", func(t *testing.T) { t.Run("can't create duplicate account", func(t *testing.T) {
_, err := service.Create(username, password, &email, &avatarURL) _, err := s.Create(username, password, &email, &avatarURL)
if err == nil { if err == nil {
t.Error("Duplicate account was created") t.Error("Duplicate account was created")
} }
@ -138,11 +156,11 @@ func Test_Account(t *testing.T) {
t.Run("can change username", func(t *testing.T) { t.Run("can change username", func(t *testing.T) {
testUsername := "some_other_name" testUsername := "some_other_name"
if err := service.ChangeUsername(id, testUsername); err != nil { if err := s.ChangeUsername(id, testUsername); err != nil {
t.Errorf("Failed to change username: %v", err) t.Errorf("Failed to change username: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -151,19 +169,21 @@ func Test_Account(t *testing.T) {
} }
t.Run("but not to an invalid value", func(t *testing.T) { t.Run("but not to an invalid value", func(t *testing.T) {
if err := service.ChangeUsername(id, ""); err == nil { if err := s.ChangeUsername(id, ""); err == nil {
t.Error("Could change username to invalid value") t.Error("Could change username to invalid value")
} else if !errors.IsValidationError(err) {
t.Error("Error is not validation error")
} }
}) })
}) })
t.Run("can change password", func(t *testing.T) { t.Run("can change password", func(t *testing.T) {
testPassword := "other more different password" testPassword := "other more different password"
if err := service.ChangePassword(id, testPassword); err != nil { if err := s.ChangePassword(id, testPassword); err != nil {
t.Errorf("Failed to change password: %v", err) t.Errorf("Failed to change password: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -172,19 +192,21 @@ func Test_Account(t *testing.T) {
} }
t.Run("but not to an invalid value", func(t *testing.T) { t.Run("but not to an invalid value", func(t *testing.T) {
if err := service.ChangePassword(id, ""); err == nil { if err := s.ChangePassword(id, ""); err == nil {
t.Error("Could change password to invalid value") t.Error("Could change password to invalid value")
} else if !errors.IsValidationError(err) {
t.Error("Error is not validation error")
} }
}) })
}) })
t.Run("can change email", func(t *testing.T) { t.Run("can change email", func(t *testing.T) {
testEmail := "brandnewemail@for.me" testEmail := "brandnewemail@for.me"
if err := service.ChangeEmail(id, testEmail); err != nil { if err := s.ChangeEmail(id, testEmail); err != nil {
t.Errorf("Failed to change email: %v", err) t.Errorf("Failed to change email: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -194,11 +216,11 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can remove email", func(t *testing.T) { t.Run("can remove email", func(t *testing.T) {
if err := service.ChangeEmail(id, ""); err != nil { if err := s.ChangeEmail(id, ""); err != nil {
t.Errorf("Failed to change email: %v", err) t.Errorf("Failed to change email: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -209,11 +231,11 @@ func Test_Account(t *testing.T) {
t.Run("can change avatar URL", func(t *testing.T) { t.Run("can change avatar URL", func(t *testing.T) {
testAvatarURL := "/img/some-other-avatar.webp" testAvatarURL := "/img/some-other-avatar.webp"
if err := service.ChangeAvatarURL(id, testAvatarURL); err != nil { if err := s.ChangeAvatarURL(id, testAvatarURL); err != nil {
t.Errorf("Failed to change avatar URL: %v", err) t.Errorf("Failed to change avatar URL: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -223,11 +245,11 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can remove avatar URL", func(t *testing.T) { t.Run("can remove avatar URL", func(t *testing.T) {
if err := service.ChangeAvatarURL(id, ""); err != nil { if err := s.ChangeAvatarURL(id, ""); err != nil {
t.Errorf("Failed to change avatar URL: %v", err) t.Errorf("Failed to change avatar URL: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -237,13 +259,13 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can increment auth failures", func(t *testing.T) { t.Run("can increment auth failures", func(t *testing.T) {
if num, err := service.IncrementFails(id); err != nil { if num, err := s.IncrementFails(id); err != nil {
t.Errorf("Failed to increment account auth failures: %v", err) t.Errorf("Failed to increment account auth failures: %v", err)
} else { } else {
assert.Equal(t, num, 1) assert.Equal(t, num, 1)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -253,11 +275,11 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can reset auth failures", func(t *testing.T) { t.Run("can reset auth failures", func(t *testing.T) {
if err := service.ResetFails(id); err != nil { if err := s.ResetFails(id); err != nil {
t.Errorf("Failed to reset account auth failures: %v", err) t.Errorf("Failed to reset account auth failures: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -267,11 +289,11 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can lock account", func(t *testing.T) { t.Run("can lock account", func(t *testing.T) {
if err := service.Lock(id); err != nil { if err := s.Lock(id); err != nil {
t.Errorf("Failed to lock account: %v", err) t.Errorf("Failed to lock account: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -281,11 +303,11 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can unlock account", func(t *testing.T) { t.Run("can unlock account", func(t *testing.T) {
if err := service.Unlock(id); err != nil { if err := s.Unlock(id); err != nil {
t.Errorf("Failed to unlock account: %v", err) t.Errorf("Failed to unlock account: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err) t.Errorf("Failed to get account: %v", err)
} else if account == nil { } else if account == nil {
t.Error("Account is nil after update") t.Error("Account is nil after update")
@ -295,12 +317,14 @@ func Test_Account(t *testing.T) {
}) })
t.Run("can delete account", func(t *testing.T) { t.Run("can delete account", func(t *testing.T) {
if err = service.Delete(id); err != nil { if err = s.Delete(id); err != nil {
t.Errorf("Failed to delete account: %v", err) t.Errorf("Failed to delete account: %v", err)
} }
if account, err := service.GetByID(id); err != nil { if account, err := s.GetByID(id); err != nil {
if !errors.IsNotExistError(err) {
t.Errorf("Failed to get account after deletion: %v", err) t.Errorf("Failed to get account after deletion: %v", err)
}
} else if account != nil { } else if account != nil {
t.Error("Account still exists after deletion") t.Error("Account still exists after deletion")
} }
@ -308,56 +332,76 @@ func Test_Account(t *testing.T) {
t.Run("can't create an account with invalid", func(t *testing.T) { t.Run("can't create an account with invalid", func(t *testing.T) {
t.Run("username", func(t *testing.T) { t.Run("username", func(t *testing.T) {
if _, err := service.Create("", password, &email, &avatarURL); err == nil { if _, err := s.Create("", password, &email, &avatarURL); err == nil {
t.Error("Could create account with empty username") t.Error("Could create account with empty username")
} }
}) })
t.Run("password", func(t *testing.T) { t.Run("password", func(t *testing.T) {
if _, err := service.Create(username, "", &email, &avatarURL); err == nil { if _, err := s.Create(username, "", &email, &avatarURL); err == nil {
t.Error("Could create account with empty password") t.Error("Could create account with empty password")
} }
}) })
t.Run("email", func(t *testing.T) { t.Run("email", func(t *testing.T) {
testEmail := "" testEmail := ""
if _, err := service.Create(username, password, &testEmail, &avatarURL); err == nil { if _, err := s.Create(username, password, &testEmail, &avatarURL); err == nil {
t.Error("Could create account with empty (non-nil) email") t.Error("Could create account with empty (non-nil) email")
} }
}) })
}) })
t.Run("can't fetch account that doesn't exist", func(t *testing.T) {
t.Run("by ID", func(t *testing.T) {
if account, err := s.GetByID("adsginh534g9405gmb40i9bm"); err != nil {
if !errors.IsNotExistError(err) { t.Errorf("Failed to get account: %v", err) }
} else if account != nil {
t.Error("Could fetch non-existent account")
}
})
t.Run("by username", func(t *testing.T) {
if account, err := s.GetByUsername("adsginh534g9405gmb40i9bm"); err != nil {
if !errors.IsNotExistError(err) { t.Errorf("Failed to get account: %v", err) }
} else if account != nil {
t.Error("Could fetch non-existent account")
}
})
t.Run("email", func(t *testing.T) {
if account, err := s.GetByEmail("adsginh534g9405gmb40i9bm"); err != nil {
if !errors.IsNotExistError(err) { t.Errorf("Failed to get account: %v", err) }
} else if account != nil {
t.Error("Could fetch non-existent account")
}
})
})
t.Run("can't update account that doesn't exist", func(t *testing.T) { t.Run("can't update account that doesn't exist", func(t *testing.T) {
garbageAccountID := "adsginh534g9405gmb40i9bm" garbageAccountID := "adsginh534g9405gmb40i9bm"
t.Run("username", func(t *testing.T) { t.Run("username", func(t *testing.T) {
if err := service.ChangeUsername(garbageAccountID, "some-username"); err == nil { if err := s.ChangeUsername(garbageAccountID, "some-username"); err == nil {
t.Error("Could update non-existent account's username") t.Error("Could update non-existent account's username")
} }
}) })
t.Run("password", func(t *testing.T) { t.Run("password", func(t *testing.T) {
if err := service.ChangePassword(garbageAccountID, "some-password"); err == nil { if err := s.ChangePassword(garbageAccountID, "some-password"); err == nil {
t.Error("Could update non-existent account's password") t.Error("Could update non-existent account's password")
} }
}) })
t.Run("email", func(t *testing.T) { t.Run("email", func(t *testing.T) {
if err := service.ChangeEmail(garbageAccountID, "some-email@real.gov"); err == nil { if err := s.ChangeEmail(garbageAccountID, "some-email@real.gov"); err == nil {
t.Error("Could update non-existent account's email") t.Error("Could update non-existent account's email")
} }
}) })
t.Run("avatar URL", func(t *testing.T) { t.Run("avatar URL", func(t *testing.T) {
if err := service.ChangeAvatarURL(garbageAccountID, "/img/null.webp"); err == nil { if err := s.ChangeAvatarURL(garbageAccountID, "/img/null.webp"); err == nil {
t.Error("Could update non-existent account's avatar URL") t.Error("Could update non-existent account's avatar URL")
} }
}) })
}) })
t.Cleanup(func() {
if err := service.Delete(id); err != nil {
t.Errorf("Failed to clean up test case: %v", err)
}
})
} }

View file

@ -3,6 +3,7 @@ package log
import ( import (
"arimelody-web/model" "arimelody-web/model"
repository "arimelody-web/repository/log" repository "arimelody-web/repository/log"
"arimelody-web/errors"
"fmt" "fmt"
"log" "log"
"time" "time"
@ -43,7 +44,10 @@ func (s *LogService) Warn(logType string, format string, args ...any) {
} }
func (s *LogService) Fetch(id string) (*model.Log, error) { func (s *LogService) Fetch(id string) (*model.Log, error) {
return s.repo.Get(id) log, err := s.repo.Get(id)
if err != nil { return nil, err }
if log == nil { return nil, errors.NewNotExistError("Log does not exist") }
return log, nil
} }
func (s *LogService) Search( func (s *LogService) Search(

82
service/music/artist.go Normal file
View file

@ -0,0 +1,82 @@
package music
import (
"arimelody-web/errors"
"arimelody-web/model"
"arimelody-web/service/validator"
)
func (s *MusicService) GetAllArtists() ([]*model.Artist, error) {
return s.repo.GetAllArtists()
}
func (s *MusicService) GetArtistCount() (int, error) {
return s.repo.GetArtistCount()
}
func (s *MusicService) GetArtistByID(id string) (*model.Artist, error) {
artist, err := s.repo.GetArtistByID(id)
if err != nil { return nil, err }
if artist == nil { return nil, errors.NewNotExistError("Artist does not exist") }
return artist, nil
}
func (s *MusicService) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
return s.repo.GetArtistsNotOnRelease(releaseID)
}
func (s *MusicService) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) {
return s.repo.GetArtistCredits(artistID, showHidden)
}
func (s *MusicService) CreateArtist(
id string,
name string,
website string,
avatar string,
) error {
if len(id) == 0 { return errors.NewValidationError("Artist ID cannot be empty") }
if !validator.ValidateID(id) { return errors.NewValidationError("Artist ID contains invalid characters") }
if len(name) == 0 { return errors.NewValidationError("Artist name cannot be empty") }
if err := s.repo.CreateArtist(id, name, website, avatar); err != nil { return err }
s.log.Printf("Created new artist '%s' (%s)", name, id)
return nil
}
func (s *MusicService) UpdateArtist(artist *model.Artist) error {
if len(artist.ID) == 0 { return errors.NewValidationError("Artist ID cannot be empty") }
if !validator.ValidateID(artist.ID) { return errors.NewValidationError("Artist ID contains invalid characters") }
if len(artist.Name) == 0 { return errors.NewValidationError("Artist name cannot be empty") }
if err := s.repo.UpdateArtist(artist); err != nil { return err }
s.log.Printf("Updated artist %s", artist.ID)
return nil
}
func (s *MusicService) UpdateArtistID(oldID string, newID string) error {
if len(newID) == 0 { return errors.NewValidationError("Artist ID cannot be empty") }
if !validator.ValidateID(newID) { return errors.NewValidationError("Artist ID contains invalid characters") }
if err := s.repo.UpdateArtistID(oldID, newID); err != nil { return err }
s.log.Printf("Updated artist ID %s to %s", oldID, newID)
return nil
}
func (s *MusicService) UpdateArtistName(id string, name string) error {
if len(name) == 0 { return errors.NewValidationError("Artist name cannot be empty") }
if err := s.repo.UpdateArtistName(id, name); err != nil { return err }
s.log.Printf("Updated artist %s name to %s", id, name)
return nil
}
func (s *MusicService) UpdateArtistWebsite(id string, website string) error {
if err := s.repo.UpdateArtistWebsite(id, website); err != nil { return err }
s.log.Printf("Updated artist %s website to %s", id, website)
return nil
}
func (s *MusicService) UpdateArtistAvatar(id string, avatar string) error {
if err := s.repo.UpdateArtistAvatar(id, avatar); err != nil { return err }
s.log.Printf("Updated artist %s avatar to %s", id, avatar)
return nil
}
func (s *MusicService) DeleteArtist(id string) error {
deletedID, err := s.repo.DeleteArtist(id)
if err != nil { return err }
if deletedID == "" { return errors.NewNotExistError("Artist does not exist") }
s.log.Printf("Deleted artist %s", id)
return nil
}

View file

@ -0,0 +1,225 @@
package music_test
import (
"arimelody-web/errors"
"arimelody-web/model"
repository "arimelody-web/repository/music"
service "arimelody-web/service/music"
"log"
"os"
"slices"
"testing"
"time"
"gotest.tools/v3/assert"
)
func Test_Artist(t *testing.T) {
devNullFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
if err != nil { panic(err) }
defer devNullFile.Close()
repo := repository.NewMusicRepositoryMemory(
make([]*model.Artist, 0),
make([]*model.Release, 0),
make([]*model.Track, 0),
)
s = service.NewMusicService(
repo,
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
)
id := "cool-artist"
name := "Cool Artist"
website := "artist.arimelody.space"
avatarURL := "/img/cool-artist.webp"
t.Run("artists should start empty", func(t *testing.T) {
t.Run("count is zero", func(t *testing.T) {
if num, err := s.GetArtistCount(); err != nil {
t.Errorf("Failed to get number of artists: %v", err)
} else {
assert.Equal(t, num, 0)
}
})
t.Run("service returns empty array", func(t *testing.T) {
if artists, err := s.GetAllArtists(); err != nil {
t.Errorf("Failed to get artists: %v", err)
} else {
assert.Equal(t, len(artists), 0)
}
})
})
t.Run("can create artist", func(t *testing.T) {
if err := s.CreateArtist(id, name, website, avatarURL); err != nil {
t.Errorf("Failed to create artist: %v", err)
}
t.Run("but not with an invalid ID", func(t *testing.T) {
if err := s.CreateArtist("", name, website, avatarURL); err == nil {
t.Error("Created artist with invalid ID")
}
})
t.Run("but not with an invalid name", func(t *testing.T) {
if err := s.CreateArtist("test-artist", "", website, avatarURL); err == nil {
t.Error("Created artist with invalid name")
}
})
t.Run("and retrieve it", func(t *testing.T) {
repoArtist, err := s.GetArtistByID(id)
if err != nil { t.Errorf("Failed to get artist: %v", err) }
assert.Equal(t, repoArtist.ID, id)
assert.Equal(t, repoArtist.Name, name)
assert.Equal(t, repoArtist.Website, website)
assert.Equal(t, repoArtist.Avatar, avatarURL)
})
})
t.Run("number of artists should increment", func(t *testing.T) {
t.Run("count is one", func(t *testing.T) {
if num, err := s.GetArtistCount(); err != nil {
t.Errorf("Failed to get number of artists: %v", err)
} else {
assert.Equal(t, num, 1)
}
})
t.Run("service returns array with one artist", func(t *testing.T) {
if artists, err := s.GetAllArtists(); err != nil {
t.Errorf("Failed to get artists: %v", err)
} else {
assert.Equal(t, len(artists), 1)
}
})
})
releaseID := "test-release"
t.Run("can get artists not on a release", func(t *testing.T) {
if err := s.CreateRelease(releaseID, "test", string(model.Single), time.Now(), ""); err != nil {
t.Errorf("Failed to create release: %v", err)
}
artists, err := s.GetArtistsNotOnRelease(releaseID)
if err != nil { t.Errorf("Failed to get artists: %v", err) }
assert.Assert(t, slices.ContainsFunc(artists, func(artist *model.Artist) bool {
return artist.ID == id
}))
})
credit := &model.Credit{
Artist: &model.Artist{ ID: id, Name: name, Website: website, Avatar: avatarURL },
Role: "did a lot of stuff",
Primary: true,
}
t.Run("can get artist credits", func(t *testing.T) {
if err := s.UpdateReleaseCredits(releaseID, []*model.Credit{ credit }); err != nil {
t.Errorf("Failed to update release credits: %v", err)
}
credits, err := s.GetArtistCredits(id, true)
if err != nil { t.Errorf("Failed to get credits: %v", err) }
index := slices.IndexFunc(credits, func(credit *model.Credit) bool {
return credit.Artist.ID == id
})
assert.Assert(t, index != -1)
assert.Equal(t, credits[index].Artist.ID, id)
assert.Equal(t, credits[index].Artist.Name, name)
assert.Equal(t, credits[index].Artist.Website, website)
assert.Equal(t, credits[index].Artist.Avatar, avatarURL)
assert.Equal(t, credits[index].Role, credit.Role)
assert.Equal(t, credits[index].Primary, credit.Primary)
})
t.Run("can update artist", func(t *testing.T) {
testName := "this name is only temporary"
testWebsite := "https://test.example.org"
testAvatar := "/img/test-avatar.webp"
if err := s.UpdateArtist(&model.Artist{
ID: id,
Name: testName,
Website: testWebsite,
Avatar: testAvatar,
}); err != nil {
t.Errorf("Failed to update artist: %v", err)
}
artist, err := s.GetArtistByID(id)
if err != nil { t.Errorf("Failed to get artist: %v", err) }
assert.Equal(t, artist.Name, testName)
assert.Equal(t, artist.Website, testWebsite)
assert.Equal(t, artist.GetAvatar(), testAvatar)
t.Run("but not with invalid name", func(t *testing.T) {
if err := s.UpdateArtist(&model.Artist{
ID: id,
Name: "",
}); err == nil {
t.Errorf("Updated artist name to invalid value")
}
})
})
t.Run("can update artist name", func(t *testing.T) {
if err := s.UpdateArtistName(id, name); err != nil {
t.Errorf("Failed to update artist: %v", err)
}
artist, err := s.GetArtistByID(id)
if err != nil { t.Errorf("Failed to get artist: %v", err) }
assert.Equal(t, artist.Name, name)
t.Run("but not with invalid value", func(t *testing.T) {
if err := s.UpdateArtistName(id, ""); err == nil {
t.Errorf("Updated artist name to invalid value")
}
})
})
t.Run("can update artist website", func(t *testing.T) {
if err := s.UpdateArtistWebsite(id, website); err != nil {
t.Errorf("Failed to update artist: %v", err)
}
artist, err := s.GetArtistByID(id)
if err != nil { t.Errorf("Failed to get artist: %v", err) }
assert.Equal(t, artist.Website, website)
})
t.Run("can update artist avatar", func(t *testing.T) {
if err := s.UpdateArtistAvatar(id, ""); err != nil {
t.Errorf("Failed to update artist: %v", err)
}
artist, err := s.GetArtistByID(id)
if err != nil { t.Errorf("Failed to get artist: %v", err) }
assert.Equal(t, artist.GetAvatar(), model.DEFAULT_AVATAR_URL)
})
t.Run("can delete artist", func(t *testing.T) {
if err := s.DeleteArtist(id); err != nil {
t.Errorf("Failed to delete artist: %v", err)
}
t.Run("no longer exists", func(t *testing.T) {
if artist, err := s.GetArtistByID(id); err == nil {
t.Error("No error getting artist")
} else if artist != nil {
t.Error("Artist with this ID still exists")
}
})
t.Run("but not one that doesn't exist", func(t *testing.T) {
if err := s.DeleteArtist("some-garbage-id"); err == nil {
if !errors.IsNotExistError(err) {
t.Errorf("Failed to delete artist: %v", err)
}
}
})
})
}

18
service/music/music.go Normal file
View file

@ -0,0 +1,18 @@
package music
import (
repository "arimelody-web/repository/music"
"log"
)
type MusicService struct {
repo repository.MusicRepository
log *log.Logger
}
func NewMusicService(repo repository.MusicRepository, logger *log.Logger) *MusicService {
return &MusicService{
repo: repo,
log: logger,
}
}

View file

@ -0,0 +1,9 @@
package music_test
import (
service "arimelody-web/service/music"
)
var (
s *service.MusicService
)

180
service/music/release.go Normal file
View file

@ -0,0 +1,180 @@
package music
import (
"arimelody-web/errors"
"arimelody-web/model"
"arimelody-web/service/validator"
"fmt"
"time"
)
// Hydrates a release with its additional data (credits, tracks, links)
func (s *MusicService) fillRelease(release *model.Release) error {
credits, err := s.GetReleaseCredits(release.ID)
if err != nil { return fmt.Errorf("credits: %s", err) }
release.Credits = append(release.Credits, credits...)
tracks, err := s.GetReleaseTracks(release.ID)
if err != nil { return fmt.Errorf("tracks: %s", err) }
release.Tracks = append(release.Tracks, tracks...)
links, err := s.GetReleaseLinks(release.ID)
if err != nil { return fmt.Errorf("links: %s", err) }
release.Links = append(release.Links, links...)
return nil
}
func (s *MusicService) GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
return s.repo.GetAllReleases(onlyVisible, limit)
}
func (s *MusicService) GetAllFullReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
releases, err := s.repo.GetAllReleases(onlyVisible, limit)
if err != nil { return nil, err }
for _, release := range releases {
if err := s.fillRelease(release); err != nil { return nil, err }
}
return releases, nil
}
func (s *MusicService) GetReleaseCount(onlyVisible bool) (int, error) {
return s.repo.GetReleaseCount(onlyVisible)
}
func (s *MusicService) GetReleaseByID(id string) (*model.Release, error) {
release, err := s.repo.GetReleaseByID(id)
if err != nil { return nil, err }
if release == nil { return nil, errors.NewNotExistError("Release does not exist") }
return release, nil
}
func (s *MusicService) GetFullReleaseByID(id string) (*model.Release, error) {
release, err := s.GetReleaseByID(id)
if err != nil { return nil, err }
if err := s.fillRelease(release); err != nil { return nil, err }
return release, nil
}
func (s *MusicService) GetReleaseTracks(id string) ([]*model.Track, error) {
return s.repo.GetReleaseTracks(id)
}
func (s *MusicService) GetReleaseCredits(id string) ([]*model.Credit, error) {
return s.repo.GetReleaseCredits(id)
}
func (s *MusicService) GetReleaseLinks(id string) ([]*model.Link, error) {
return s.repo.GetReleaseLinks(id)
}
func (s *MusicService) CreateRelease(
id string,
title string,
releaseType string,
releaseDate time.Time,
artworkURL string,
) error {
if len(id) == 0 { return errors.NewValidationError("Release ID cannot be empty") }
if !validator.ValidateID(id) { return errors.NewValidationError("Release ID contains invalid characters") }
if len(title) == 0 { return errors.NewValidationError("Release title cannot be empty") }
validReleaseType, ok := model.ValidReleaseType(releaseType)
if !ok { return errors.NewValidationError("Invalid release type") }
if err := s.repo.CreateRelease(id, title, validReleaseType, releaseDate, artworkURL); err != nil { return err }
s.log.Printf("Created new release '%s' (%s)", title, id)
return nil
}
func (s *MusicService) UpdateRelease(release *model.Release) error {
if len(release.ID) == 0 { return errors.NewValidationError("Release ID cannot be empty") }
if !validator.ValidateID(release.ID) { return errors.NewValidationError("Release ID contains invalid characters") }
if len(release.Title) == 0 { return errors.NewValidationError("Release title cannot be empty") }
if err := s.repo.UpdateRelease(release); err != nil { return err }
s.log.Printf("Updated release '%s' (%s)", release.Title, release.ID)
return nil
}
func (s *MusicService) UpdateReleaseID(oldID string, newID string) error {
if len(newID) == 0 { return errors.NewValidationError("Release ID cannot be empty") }
if !validator.ValidateID(newID) { return errors.NewValidationError("Release ID contains invalid characters") }
if err := s.repo.UpdateReleaseID(oldID, newID); err != nil { return err }
s.log.Printf("Updated release ID %s to %s", oldID, newID)
return nil
}
func (s *MusicService) UpdateReleaseVisibility(id string, visible bool) error {
if err := s.repo.UpdateReleaseVisibility(id, visible); err != nil { return err }
s.log.Printf("Updated release '%s' visibility to %t", id, visible)
return nil
}
func (s *MusicService) UpdateReleaseTitle(id string, title string) error {
if len(title) == 0 { return errors.NewValidationError("Release title cannot be empty") }
if err := s.repo.UpdateReleaseTitle(id, title); err != nil { return err }
s.log.Printf("Updated release '%s' title to %s", id, title)
return nil
}
func (s *MusicService) UpdateReleaseDescription(id string, description string) error {
if err := s.repo.UpdateReleaseDescription(id, description); err != nil { return err }
s.log.Printf("Updated release '%s' description to %s", id, description)
return nil
}
func (s *MusicService) UpdateReleaseType(id string, releaseType string) error {
validReleaseType, ok := model.ValidReleaseType(releaseType)
if !ok { return errors.NewValidationError("Invalid release type") }
if err := s.repo.UpdateReleaseType(id, validReleaseType); err != nil { return err }
s.log.Printf("Updated release '%s' type to %s", id, releaseType)
return nil
}
func (s *MusicService) UpdateReleaseDate(id string, releaseDate time.Time) error {
if err := s.repo.UpdateReleaseDate(id, releaseDate); err != nil { return err }
s.log.Printf("Updated release '%s' date to %s", id, releaseDate.Format(time.RFC3339))
return nil
}
func (s *MusicService) UpdateReleaseArtwork(id string, artwork string) error {
if err := s.repo.UpdateReleaseArtwork(id, artwork); err != nil { return err }
s.log.Printf("Updated release '%s' artwork to %s", id, artwork)
return nil
}
func (s *MusicService) UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error {
if err := s.repo.UpdateReleaseBuyInfo(id, buyName, buyLink); err != nil { return err }
s.log.Printf("Updated release '%s' buy info (name='%s', link='%s')", id, buyName, buyLink)
return nil
}
func (s *MusicService) UpdateReleaseCopyright(id string, copyright string, url string) error {
if err := s.repo.UpdateReleaseCopyright(id, copyright, url); err != nil { return err }
s.log.Printf("Updated release '%s' copyright (copyright='%s', url='%s')", id, copyright, url)
return nil
}
func (s *MusicService) UpdateReleaseTracks(id string, newTrackIDs []string) error {
for _, id := range newTrackIDs {
if len(id) == 0 { return errors.NewValidationError("Track IDs cannot be empty") }
}
if err := s.repo.UpdateReleaseTracks(id, newTrackIDs); err != nil { return err }
s.log.Printf("Updated release '%s' tracks (%d tracks)", id, len(newTrackIDs))
return nil
}
func (s *MusicService) UpdateReleaseCredits(id string, newCredits []*model.Credit) error {
if err := s.repo.UpdateReleaseCredits(id, newCredits); err != nil { return err }
s.log.Printf("Updated release '%s' credits (%d credits)", id, len(newCredits))
return nil
}
func (s *MusicService) UpdateReleaseLinks(id string, newLinks []*model.Link) error {
for _, link := range newLinks {
if len(link.Name) == 0 { return errors.NewValidationError("Link names cannot be empty") }
if len(link.URL) == 0 { return errors.NewValidationError("Link URLs cannot be empty") }
}
if err := s.repo.UpdateReleaseLinks(id, newLinks); err != nil { return err }
s.log.Printf("Updated release '%s' links (%d links)", id, len(newLinks))
return nil
}
func (s *MusicService) DeleteRelease(id string) error {
deletedID, err := s.repo.DeleteRelease(id)
if err != nil { return err }
if deletedID == "" { return errors.NewNotExistError("Release does not exist") }
s.log.Printf("Deleted release '%s'", id)
return nil
}

View file

@ -0,0 +1,374 @@
package music_test
import (
"arimelody-web/errors"
"arimelody-web/model"
repository "arimelody-web/repository/music"
service "arimelody-web/service/music"
"log"
"os"
"slices"
"testing"
"time"
"gotest.tools/v3/assert"
)
func Test_Release(t *testing.T) {
devNullFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
if err != nil { panic(err) }
defer devNullFile.Close()
repo := repository.NewMusicRepositoryMemory(
make([]*model.Artist, 0),
make([]*model.Release, 0),
make([]*model.Track, 0),
)
s = service.NewMusicService(
repo,
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
)
id := "cool-release"
title := "Cool Release"
releaseType := model.Album
releaseDate := time.Now()
artworkURL := "/img/some-cool-artwork.webp"
t.Run("releases should start empty", func(t *testing.T) {
t.Run("count is zero", func(t *testing.T) {
if num, err := s.GetReleaseCount(false); err != nil {
t.Errorf("Failed to get number of releases: %v", err)
} else {
assert.Equal(t, num, 0)
}
})
t.Run("service returns empty array", func(t *testing.T) {
if releases, err := s.GetAllReleases(false, 0); err != nil {
t.Errorf("Failed to get releases: %v", err)
} else {
assert.Equal(t, len(releases), 0)
}
})
})
t.Run("can create release", func(t *testing.T) {
if err := s.CreateRelease(id, title, string(releaseType), releaseDate, artworkURL); err != nil {
t.Errorf("Failed to create release: %v", err)
}
t.Run("but not with an invalid ID", func(t *testing.T) {
if err := s.CreateRelease("", title, string(releaseType), releaseDate, artworkURL); err == nil {
t.Error("Created release with invalid ID")
}
})
t.Run("but not with an invalid title", func(t *testing.T) {
if err := s.CreateRelease("test-release", "", string(releaseType), releaseDate, artworkURL); err == nil {
t.Error("Created release with invalid title")
}
})
t.Run("but not with an invalid type", func(t *testing.T) {
if err := s.CreateRelease("test-release", "", "garbage-type", releaseDate, artworkURL); err == nil {
t.Error("Created release with invalid type")
}
})
t.Run("and retrieve it", func(t *testing.T) {
repoRelease, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if repoRelease == nil { t.Error("Release does not exist") }
assert.Equal(t, repoRelease.ID, id)
assert.Equal(t, repoRelease.Title, title)
assert.Equal(t, repoRelease.ReleaseType, releaseType)
assert.Equal(t, repoRelease.ReleaseDate, releaseDate)
assert.Equal(t, repoRelease.GetArtwork(), artworkURL)
})
t.Run("should not be visible by default", func(t *testing.T) {
releases, err := s.GetAllReleases(true, 0)
if err != nil { t.Errorf("Failed to get releases: %v", err) }
assert.Equal(t, slices.ContainsFunc(releases, func(release *model.Release) bool {
return release.ID == id
}), false)
})
})
t.Run("number of releases should increment", func(t *testing.T) {
t.Run("count is one", func(t *testing.T) {
if num, err := s.GetReleaseCount(false); err != nil {
t.Errorf("Failed to get number of releases: %v", err)
} else {
assert.Equal(t, num, 1)
}
})
t.Run("service returns array with one release", func(t *testing.T) {
if releases, err := s.GetAllReleases(false, 0); err != nil {
t.Errorf("Failed to get releases: %v", err)
} else {
assert.Equal(t, len(releases), 1)
}
})
})
t.Run("can update release", func(t *testing.T) {
testTitle := "this title is only temporary"
testReleaseType := model.Compilation
testReleaseDate := time.Now().AddDate(0, 0, 10)
testArtworkURL := "/img/test-artwork.webp"
if err := s.UpdateRelease(&model.Release{
ID: id,
Title: testTitle,
ReleaseType: testReleaseType,
ReleaseDate: testReleaseDate,
Artwork: testArtworkURL,
}); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.Title, testTitle)
assert.Equal(t, release.ReleaseType, testReleaseType)
assert.Equal(t, release.ReleaseDate, testReleaseDate)
assert.Equal(t, release.GetArtwork(), testArtworkURL)
t.Run("but not with invalid title", func(t *testing.T) {
if err := s.UpdateRelease(&model.Release{
ID: id,
Title: "",
}); err == nil {
t.Errorf("Updated release title to invalid value")
}
})
})
t.Run("can update visibility", func(t *testing.T) {
if err := s.UpdateReleaseVisibility(id, true); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.Visible, true)
t.Run("should be visible after updating", func(t *testing.T) {
releases, err := s.GetAllReleases(true, 0)
if err != nil { t.Errorf("Failed to get releases: %v", err) }
assert.Equal(t, slices.ContainsFunc(releases, func(release *model.Release) bool {
return release.ID == id
}), true)
})
})
t.Run("can update title", func(t *testing.T) {
if err := s.UpdateReleaseTitle(id, title); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.Title, title)
t.Run("but not with invalid value", func(t *testing.T) {
if err := s.UpdateReleaseTitle(id, ""); err == nil {
t.Errorf("Updated release title to invalid value")
}
})
})
t.Run("can update description", func(t *testing.T) {
testDescription := "an incredible and thought-provoking description"
if err := s.UpdateReleaseDescription(id, testDescription); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.Description, testDescription)
})
t.Run("can update type", func(t *testing.T) {
testType := model.EP
if err := s.UpdateReleaseType(id, string(testType)); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.ReleaseType, testType)
releaseType = testType
t.Run("but not with invalid value", func(t *testing.T) {
if err := s.UpdateReleaseType(id, "garbage-type"); err == nil {
t.Errorf("Updated release type to invalid value")
}
})
})
t.Run("can update date", func(t *testing.T) {
testDate := time.Now().Add(time.Hour * 24)
if err := s.UpdateReleaseDate(id, testDate); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.ReleaseDate, testDate)
releaseDate = testDate
})
t.Run("can update artwork", func(t *testing.T) {
if err := s.UpdateReleaseArtwork(id, ""); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
})
t.Run("can update buy info", func(t *testing.T) {
testBuyName := "get it now!!!"
testBuyLink := "https://arimelody.space/music"
if err := s.UpdateReleaseBuyInfo(id, testBuyName, testBuyLink); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.Buyname, testBuyName)
assert.Equal(t, release.Buylink, testBuyLink)
})
t.Run("can update copyright info", func(t *testing.T) {
testCopyright := "CC BY-SA 4.0"
testCopyrightURL := "https://creativecommons.org/licenses/by-sa/4.0/"
if err := s.UpdateReleaseCopyright(id, testCopyright, testCopyrightURL); err != nil {
t.Errorf("Failed to update release: %v", err)
}
release, err := s.GetReleaseByID(id)
if err != nil { t.Errorf("Failed to get release: %v", err) }
if release == nil { t.Error("Release does not exist after update") }
assert.Equal(t, release.Copyright, testCopyright)
assert.Equal(t, release.CopyrightURL, testCopyrightURL)
})
track := &model.Track{
Title: "test track",
Description: "average description",
Lyrics: "some lyrics",
}
t.Run("can update tracks", func(t *testing.T) {
if trackID, err := s.CreateTrack(track.Title, track.Description, track.Lyrics, ""); err != nil {
t.Errorf("Failed to create track: %v", err)
} else { track.ID = trackID }
if err := s.UpdateReleaseTracks(id, []string{ track.ID }); err != nil {
t.Errorf("Failed to update release tracks: %v", err)
}
tracks, err := s.GetReleaseTracks(id)
if err != nil { t.Errorf("Failed to get tracks: %v", err) }
index := slices.IndexFunc(tracks, func(repoTrack *model.Track) bool {
return repoTrack.ID == track.ID
})
assert.Assert(t, index != -1)
assert.Equal(t, tracks[index].Title, track.Title)
assert.Equal(t, tracks[index].Description, track.Description)
assert.Equal(t, tracks[index].Lyrics, track.Lyrics)
})
t.Run("can update credits", func(t *testing.T) {
artist := &model.Artist{ ID: id, Name: title, Website: "", Avatar: "" }
if err := s.CreateArtist(artist.ID, artist.Name, artist.Website, artist.Avatar); err != nil {
t.Errorf("Failed to create artist: %v", err)
}
credit := &model.Credit{
Artist: artist,
Role: "did a lot of stuff",
Primary: true,
}
if err := s.UpdateReleaseCredits(id, []*model.Credit{ credit }); err != nil {
t.Errorf("Failed to update release credits: %v", err)
}
credits, err := s.GetReleaseCredits(id)
if err != nil { t.Errorf("Failed to get credits: %v", err) }
assert.Equal(t, len(credits), 1)
index := slices.IndexFunc(credits, func(repoCredit *model.Credit) bool {
return repoCredit.Artist.ID == credit.Artist.ID
})
assert.Assert(t, index != -1)
assert.Equal(t, credits[index].Release.ID, id)
assert.Equal(t, credits[index].Release.Title, title)
assert.Equal(t, credits[index].Release.ReleaseType, releaseType)
assert.Equal(t, credits[index].Release.ReleaseDate, releaseDate)
assert.Equal(t, credits[index].Release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
assert.Equal(t, credits[index].Artist.ID, credit.Artist.ID)
assert.Equal(t, credits[index].Artist.Name, credit.Artist.Name)
assert.Equal(t, credits[index].Artist.Website, credit.Artist.Website)
assert.Equal(t, credits[index].Artist.GetAvatar(), credit.Artist.GetAvatar())
assert.Equal(t, credits[index].Role, credit.Role)
assert.Equal(t, credits[index].Primary, credit.Primary)
})
t.Run("can update links", func(t *testing.T) {
link := &model.Link{
Name: "awesome link you should totally go here",
URL: "https://arimelody.space",
}
if err := s.UpdateReleaseLinks(id, []*model.Link{ link }); err != nil {
t.Errorf("Failed to update release tracks: %v", err)
}
links, err := s.GetReleaseLinks(id)
if err != nil { t.Errorf("Failed to get tracks: %v", err) }
index := slices.IndexFunc(links, func(repoLink *model.Link) bool {
return repoLink.Name == link.Name
})
assert.Assert(t, index != -1)
assert.Equal(t, links[index].URL, link.URL)
})
t.Run("can delete release", func(t *testing.T) {
if err := s.DeleteRelease(id); err != nil {
t.Errorf("Failed to delete release: %v", err)
}
t.Run("no longer exists", func(t *testing.T) {
if release, err := s.GetReleaseByID(id); err == nil {
t.Error("No error getting release")
} else if release != nil {
t.Error("Release with this ID still exists")
}
})
t.Run("but not one that doesn't exist", func(t *testing.T) {
if err := s.DeleteRelease("some-garbage-id"); err == nil {
if !errors.IsNotExistError(err) {
t.Errorf("Failed to delete release: %v", err)
}
}
})
})
}

81
service/music/track.go Normal file
View file

@ -0,0 +1,81 @@
package music
import (
"arimelody-web/model"
"arimelody-web/errors"
)
func (s *MusicService) GetAllTracks() ([]*model.Track, error) {
return s.repo.GetAllTracks()
}
func (s *MusicService) GetTrackCount() (int, error) {
return s.repo.GetTrackCount()
}
func (s *MusicService) GetTrackByID(id string) (*model.Track, error) {
track, err := s.repo.GetTrackByID(id)
if err != nil { return nil, err }
if track == nil { return nil, errors.NewNotExistError("Track does not exist") }
return track, nil
}
func (s *MusicService) GetOrphanTracks() ([]*model.Track, error) {
return s.repo.GetOrphanTracks()
}
func (s *MusicService) GetTracksNotOnRelease(releaseID string) ([]*model.Track, error) {
return s.repo.GetTracksNotOnRelease(releaseID)
}
func (s *MusicService) GetTrackReleases(trackID string) ([]*model.Release, error) {
return s.repo.GetTrackReleases(trackID)
}
func (s *MusicService) GetTrackFullReleases(trackID string) ([]*model.Release, error) {
releases, err := s.GetTrackReleases(trackID)
if err != nil { return nil, err }
for _, release := range releases {
if err := s.fillRelease(release); err != nil { return nil, err }
}
return releases, nil
}
func (s *MusicService) CreateTrack(title string, description string, lyrics string, previewURL string) (string, error) {
if len(title) == 0 { return "", errors.NewValidationError("Track title cannot be empty") }
id, err := s.repo.CreateTrack(title, description, lyrics, previewURL)
if err != nil { return "", err }
s.log.Printf("Created track '%s' (%s)", title, id)
return id, nil
}
func (s *MusicService) UpdateTrack(track *model.Track) error {
if len(track.Title) == 0 { return errors.NewValidationError("Track title cannot be empty") }
if err := s.repo.UpdateTrack(track); err != nil { return err }
s.log.Printf("Updated track '%s' (%s)", track.Title, track.ID)
return nil
}
func (s *MusicService) UpdateTrackTitle(id string, title string) error {
if len(title) == 0 { return errors.NewValidationError("Track title cannot be empty") }
if err := s.repo.UpdateTrackTitle(id, title); err != nil { return err }
s.log.Printf("Updated track %s title to '%s'", id, title)
return nil
}
func (s *MusicService) UpdateTrackDescription(id string, description string) error {
if err := s.repo.UpdateTrackDescription(id, description); err != nil { return err }
s.log.Printf("Updated track %s description to '%s'", id, description)
return nil
}
func (s *MusicService) UpdateTrackLyrics(id string, lyrics string) error {
if err := s.repo.UpdateTrackLyrics(id, lyrics); err != nil { return err }
s.log.Printf("Updated track %s lyrics to '%s'", id, lyrics)
return nil
}
func (s *MusicService) UpdateTrackPreviewURL(id string, previewURL string) error {
if err := s.repo.UpdateTrackPreviewURL(id, previewURL); err != nil { return err }
s.log.Printf("Updated track %s preview URL to '%s'", id, previewURL)
return nil
}
func (s *MusicService) DeleteTrack(id string) error {
deletedID, err := s.repo.DeleteTrack(id)
if err != nil { return err }
if deletedID == "" { return errors.NewNotExistError("Track does not exist") }
s.log.Printf("Deleted track %s", id)
return nil
}

View file

@ -0,0 +1,8 @@
package validator
import "regexp"
var idRegexp = regexp.MustCompile(`^[a-zA-Z0-9\-_\.]+$`)
func ValidateID(id string) bool {
return idRegexp.MatchString(id)
}

View file

@ -0,0 +1,39 @@
package validator_test
import (
"arimelody-web/service/validator"
"testing"
"gotest.tools/v3/assert"
)
func Test_ValidateID(t *testing.T) {
t.Run("accepts alphanumberic ID", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("abcDEF123"), true)
})
t.Run("accepts hyphens", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("a0-1b"), true)
})
t.Run("accepts underscores", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("a0_1b"), true)
})
t.Run("accepts periods", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("a0.1b"), true)
})
t.Run("rejects other characters", func(t *testing.T) {
t.Run("emoji", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("🗣️🔥‼️"), false)
})
t.Run("slashes", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("this/could/be/really/bad/for/the/router"), false)
})
t.Run("question marks", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("query?these=nuts"), false)
})
t.Run("hashes", func(t *testing.T) {
assert.Equal(t, validator.ValidateID("unnecessary#tagging"), false)
})
})
}

View file

@ -22,7 +22,7 @@ func MusicHandler(app *app.AppState) http.Handler {
return return
} }
release, err := controller.GetRelease(app.DB, r.URL.Path[1:], true) release, err := app.MusicService.GetFullReleaseByID(r.URL.Path[1:])
if err != nil { if err != nil {
http.NotFound(w, r) http.NotFound(w, r)
return return
@ -36,7 +36,7 @@ func MusicHandler(app *app.AppState) http.Handler {
func ServeCatalog(app *app.AppState) http.Handler { func ServeCatalog(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
releases, err := controller.GetAllReleases(app.DB, true, 0, true) releases, err := app.MusicService.GetAllFullReleases(true, 0)
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to pull releases for catalog: %s\n", err) fmt.Printf("WARN: Failed to pull releases for catalog: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)