diff --git a/admin/artisthttp.go b/admin/artisthttp.go
index ef67a4e..05cfbb5 100644
--- a/admin/artisthttp.go
+++ b/admin/artisthttp.go
@@ -6,7 +6,6 @@ import (
"strings"
"arimelody-web/admin/templates"
- "arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
)
@@ -23,7 +22,7 @@ func serveArtists(app *app.AppState) http.Handler {
return
}
- artists, err := controller.GetAllArtists(app.DB)
+ artists, err := app.MusicService.GetAllArtists()
if err != nil {
fmt.Printf("WARN: Failed to fetch artists: %s\n", err)
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) {
session := r.Context().Value("session").(*model.Session)
- artist, err := controller.GetArtist(app.DB, artistID)
+ artist, err := app.MusicService.GetArtistByID(artistID)
if err != nil {
if artist == nil {
http.NotFound(w, r)
@@ -61,7 +60,7 @@ func serveArtist(app *app.AppState, artistID string) http.Handler {
return
}
- credits, err := controller.GetArtistCredits(app.DB, artist.ID, true)
+ credits, err := app.MusicService.GetArtistCredits(artistID, true)
if err != nil {
fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/admin/http.go b/admin/http.go
index 97437ac..66e66f1 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -83,39 +83,39 @@ func AdminIndexHandler(app *app.AppState) http.Handler {
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 {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- releaseCount, err := controller.GetReleaseCount(app.DB, false)
+ releaseCount, err := app.MusicService.GetReleaseCount(false)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- artists, err := controller.GetAllArtists(app.DB)
+ artists, err := app.MusicService.GetAllArtists()
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- artistCount, err := controller.GetArtistCount(app.DB)
+ artistCount, err := app.MusicService.GetArtistCount()
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- tracks, err := controller.GetOrphanTracks(app.DB)
+ tracks, err := app.MusicService.GetOrphanTracks()
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- trackCount, err := controller.GetTrackCount(app.DB)
+ trackCount, err := app.MusicService.GetTrackCount()
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/admin/releasehttp.go b/admin/releasehttp.go
index d16528e..72586ed 100644
--- a/admin/releasehttp.go
+++ b/admin/releasehttp.go
@@ -7,9 +7,9 @@ import (
"strings"
"arimelody-web/admin/templates"
- "arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
+ "arimelody-web/errors"
)
func serveReleases(app *app.AppState) http.Handler {
@@ -34,7 +34,7 @@ func serveReleases(app *app.AppState) http.Handler {
Releases []*model.Release
}
- releases, err := controller.GetAllReleases(app.DB, false, 0, true)
+ releases, err := app.MusicService.GetAllFullReleases(false, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err)
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) {
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 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)
return
}
@@ -130,7 +134,7 @@ func serveEditCredits(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) {
- artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID)
+ artists, err := app.MusicService.GetArtistsNotOnRelease(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err)
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) {
split := strings.Split(r.URL.Path, "/")
artistID := split[len(split) - 1]
- artist, err := controller.GetArtist(app.DB, artistID)
+ artist, err := app.MusicService.GetArtistByID(artistID)
if err != nil {
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
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 {
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 {
fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err)
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) {
split := strings.Split(r.URL.Path, "/")
trackID := split[len(split) - 1]
- track, err := controller.GetTrack(app.DB, trackID)
+ track, err := app.MusicService.GetTrackByID(trackID)
if err != nil {
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/admin/trackhttp.go b/admin/trackhttp.go
index 2122748..8c4554b 100644
--- a/admin/trackhttp.go
+++ b/admin/trackhttp.go
@@ -6,7 +6,6 @@ import (
"strings"
"arimelody-web/admin/templates"
- "arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
)
@@ -23,7 +22,7 @@ func serveTracks(app *app.AppState) http.Handler {
return
}
- tracks, err := controller.GetAllTracks(app.DB)
+ tracks, err := app.MusicService.GetAllTracks()
if err != nil {
fmt.Printf("WARN: Failed to fetch tracks: %s\n", err)
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) {
session := r.Context().Value("session").(*model.Session)
- track, err := controller.GetTrack(app.DB, trackID)
+ track, err := app.MusicService.GetTrackByID(trackID)
if err != nil {
fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -61,7 +60,7 @@ func serveTrack(app *app.AppState, trackID string) http.Handler {
return
}
- releases, err := controller.GetTrackReleases(app.DB, track.ID, true)
+ releases, err := app.MusicService.GetTrackReleases(trackID)
if err != nil {
fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/api/api.go b/api/api.go
index 84b7f8a..f23fcbd 100644
--- a/api/api.go
+++ b/api/api.go
@@ -10,6 +10,7 @@ import (
"arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
+ "arimelody-web/errors"
)
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) {
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 strings.Contains(err.Error(), "no rows") {
+ if errors.IsNotExistError(err) {
http.NotFound(w, r)
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) {
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 strings.Contains(err.Error(), "no rows") {
+ if errors.IsNotExistError(err) {
http.NotFound(w, r)
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) {
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 strings.Contains(err.Error(), "no rows") {
+ if errors.IsNotExistError(err) {
http.NotFound(w, r)
return
}
@@ -187,7 +188,10 @@ func getSession(app *app.AppState, r *http.Request) (*model.Session, error) {
// fetch existing session
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)
}
diff --git a/api/artist.go b/api/artist.go
index a43228d..61cb207 100644
--- a/api/artist.go
+++ b/api/artist.go
@@ -10,15 +10,15 @@ import (
"strings"
"time"
- "arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
+ "arimelody-web/errors"
)
func ServeAllArtists(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artists = []*model.Artist{}
- artists, err := controller.GetAllArtists(app.DB)
+ artists, err := app.MusicService.GetAllArtists()
if err != nil {
fmt.Printf("WARN: Failed to serve all artists: %s\n", err)
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)
- 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 {
fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err)
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) {
session := r.Context().Value("session").(*model.Session)
- var artist model.Artist
- err := json.NewDecoder(r.Body).Decode(&artist)
+ type CreateArtistDTO struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ }
+ dto := &CreateArtistDTO{}
+
+ err := json.NewDecoder(r.Body).Decode(dto)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- if artist.ID == "" {
- 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)
+ err = app.MusicService.CreateArtist(dto.ID, dto.Name, "", "")
if err != nil {
- if strings.Contains(err.Error(), "duplicate key") {
- http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest)
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
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)
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)
})
@@ -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 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)
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) {
session := r.Context().Value("session").(*model.Session)
- err := controller.DeleteArtist(app.DB, artist.ID)
+ err := app.MusicService.DeleteArtist(artist.ID)
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)
return
}
diff --git a/api/release.go b/api/release.go
index 7448a4e..e97eb16 100644
--- a/api/release.go
+++ b/api/release.go
@@ -13,6 +13,7 @@ import (
"arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/model/app"
+ "arimelody-web/errors"
)
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 (
- Track struct {
+ TrackDTO struct {
Title string `json:"title"`
Description string `json:"description"`
Lyrics string `json:"lyrics"`
}
- Credit struct {
+ CreditDTO struct {
*model.Artist
Role string `json:"role"`
Primary bool `json:"primary"`
}
- Release struct {
+ ReleaseDTO struct {
*model.Release
- Tracks []Track `json:"tracks"`
- Credits []Credit `json:"credits"`
+ Tracks []TrackDTO `json:"tracks"`
+ Credits []CreditDTO `json:"credits"`
Links map[string]string `json:"links"`
}
)
- response := Release{
+ response := ReleaseDTO{
Release: release,
- Tracks: []Track{},
- Credits: []Credit{},
+ Tracks: []TrackDTO{},
+ Credits: []CreditDTO{},
Links: make(map[string]string),
}
if release.IsReleased() || privileged {
// get credits
- credits, err := controller.GetReleaseCredits(app.DB, release.ID)
+ credits, err := app.MusicService.GetReleaseCredits(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, credit := range credits {
- artist, err := controller.GetArtist(app.DB, credit.Artist.ID)
+ artist, err := app.MusicService.GetArtistByID(credit.Artist.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- response.Credits = append(response.Credits, Credit{
+ response.Credits = append(response.Credits, CreditDTO{
Artist: artist,
Role: credit.Role,
Primary: credit.Primary,
@@ -90,14 +91,14 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
}
// get tracks
- tracks, err := controller.GetReleaseTracks(app.DB, release.ID)
+ tracks, err := app.MusicService.GetReleaseTracks(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, track := range tracks {
- response.Tracks = append(response.Tracks, Track{
+ response.Tracks = append(response.Tracks, TrackDTO{
Title: track.Title,
Description: track.Description,
Lyrics: track.Lyrics,
@@ -105,7 +106,7 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
}
// get links
- links, err := controller.GetReleaseLinks(app.DB, release.ID)
+ links, err := app.MusicService.GetReleaseLinks(release.ID)
if err != nil {
fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
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 {
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 {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@@ -192,47 +193,56 @@ func CreateRelease(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session)
- var release model.Release
- err := json.NewDecoder(r.Body).Decode(&release)
+ type CreateReleaseDTO struct {
+ 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 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- if release.ID == "" {
- http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
- return
- }
-
- if release.Title == "" { release.Title = release.ID }
- if release.ReleaseType == "" { release.ReleaseType = model.Single }
-
- if release.ReleaseDate != time.Unix(0, 0) {
- release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
+ if dto.ReleaseType == "" { dto.ReleaseType = string(model.Single) }
+ if dto.ReleaseDate != time.Unix(0, 0) {
+ 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.Artwork == "" { release.Artwork = "/img/default-cover-art.png" }
-
- err = controller.CreateRelease(app.DB, &release)
+ err = app.MusicService.CreateRelease(
+ dto.ID,
+ dto.Title,
+ dto.ReleaseType,
+ dto.ReleaseDate,
+ dto.Artwork,
+ )
if err != nil {
- if strings.Contains(err.Error(), "duplicate key") {
- http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
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)
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.WriteHeader(http.StatusCreated)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
- err = encoder.Encode(release)
+ err = encoder.Encode(dto)
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)
}
})
@@ -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 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)
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) {
session := r.Context().Value("session").(*model.Session)
- var trackIDs = []string{}
- err := json.NewDecoder(r.Body).Decode(&trackIDs)
+ var newTrackIDs = []string{}
+ err := json.NewDecoder(r.Body).Decode(&newTrackIDs)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
+ err = app.MusicService.UpdateReleaseTracks(release.ID, newTrackIDs)
if err != nil {
- if strings.Contains(err.Error(), "duplicate key") {
- http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- if strings.Contains(err.Error(), "no rows") {
+ if errors.IsNotExistError(err) {
http.NotFound(w, r)
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)
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) {
session := r.Context().Value("session").(*model.Session)
- type creditJSON struct {
+ type CreditDTO struct {
Artist string
Role string
Primary bool
}
- var data []creditJSON
- err := json.NewDecoder(r.Body).Decode(&data)
+ var dto []CreditDTO
+ err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- var credits []*model.Credit
- for _, credit := range data {
- credits = append(credits, &model.Credit{
- Artist: model.Artist{
+ var newCredits []*model.Credit
+ for _, credit := range dto {
+ newCredits = append(newCredits, &model.Credit{
+ Artist: &model.Artist{
ID: credit.Artist,
},
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 strings.Contains(err.Error(), "duplicate key") {
- http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- if strings.Contains(err.Error(), "no rows") {
+ if errors.IsNotExistError(err) {
http.NotFound(w, r)
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)
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) {
session := r.Context().Value("session").(*model.Session)
- var links = []*model.Link{}
- err := json.NewDecoder(r.Body).Decode(&links)
+ type LinkDTO struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ }
+ var dto = []LinkDTO{}
+ err := json.NewDecoder(r.Body).Decode(&dto)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
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 strings.Contains(err.Error(), "duplicate key") {
- http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- if strings.Contains(err.Error(), "no rows") {
+ if errors.IsNotExistError(err) {
http.NotFound(w, r)
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)
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) {
session := r.Context().Value("session").(*model.Session)
- err := controller.DeleteRelease(app.DB, release.ID)
+ err := app.MusicService.DeleteRelease(release.ID)
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)
return
}
diff --git a/api/track.go b/api/track.go
index c742681..749c66e 100644
--- a/api/track.go
+++ b/api/track.go
@@ -5,7 +5,7 @@ import (
"fmt"
"net/http"
- "arimelody-web/controller"
+ "arimelody-web/errors"
"arimelody-web/model"
"arimelody-web/model/app"
)
@@ -26,7 +26,7 @@ func ServeAllTracks(app *app.AppState) http.Handler {
var tracks = []Track{}
var dbTracks = []*model.Track{}
- dbTracks, err := controller.GetAllTracks(app.DB)
+ dbTracks, err := app.MusicService.GetAllTracks()
if err != nil {
fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err)
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 {
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 {
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)
@@ -78,26 +78,30 @@ func CreateTrack(app *app.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session)
- var track model.Track
- err := json.NewDecoder(r.Body).Decode(&track)
+ type CreateTrackDTO struct {
+ 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 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- if track.Title == "" {
- http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest)
- return
- }
-
- id, err := controller.CreateTrack(app.DB, &track)
+ id, err := app.MusicService.CreateTrack(dto.Title, dto.Description, dto.Lyrics, "")
if err != nil {
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
fmt.Printf("WARN: Failed to create track: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
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.WriteHeader(http.StatusCreated)
@@ -114,19 +118,28 @@ func UpdateTrack(app *app.AppState, track *model.Track) http.Handler {
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 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- if track.Title == "" {
- http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest)
- return
- }
+ track.Title = dto.Title
+ track.Description = dto.Description
+ track.Lyrics = dto.Lyrics
- err = controller.UpdateTrack(app.DB, track)
+ err = app.MusicService.UpdateTrack(track)
if err != nil {
+ if errors.IsValidationError(err) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@@ -154,8 +167,12 @@ func DeleteTrack(app *app.AppState, track *model.Track) http.Handler {
session := r.Context().Value("session").(*model.Session)
var trackID = r.URL.Path[1:]
- err := controller.DeleteTrack(app.DB, trackID)
+ err := app.MusicService.DeleteTrack(trackID)
if err != nil {
+ if errors.IsNotExistError(err) {
+ http.NotFound(w, r)
+ return
+ }
fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
diff --git a/controller/release.go b/controller/release.go
deleted file mode 100644
index a22d157..0000000
--- a/controller/release.go
+++ /dev/null
@@ -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
-}
diff --git a/controller/track.go b/controller/track.go
deleted file mode 100644
index 27f4afc..0000000
--- a/controller/track.go
+++ /dev/null
@@ -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
-}
diff --git a/errors/notexist.go b/errors/notexist.go
new file mode 100644
index 0000000..01b0fc2
--- /dev/null
+++ b/errors/notexist.go
@@ -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
+}
+
diff --git a/errors/notexist_test.go b/errors/notexist_test.go
new file mode 100644
index 0000000..fe61771
--- /dev/null
+++ b/errors/notexist_test.go
@@ -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)
+ })
+}
diff --git a/errors/validation.go b/errors/validation.go
new file mode 100644
index 0000000..7309694
--- /dev/null
+++ b/errors/validation.go
@@ -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
+}
diff --git a/errors/validation_test.go b/errors/validation_test.go
new file mode 100644
index 0000000..8a6783c
--- /dev/null
+++ b/errors/validation_test.go
@@ -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)
+ })
+}
diff --git a/main.go b/main.go
index f0d7f95..7086265 100644
--- a/main.go
+++ b/main.go
@@ -24,12 +24,16 @@ import (
"arimelody-web/cursor"
"arimelody-web/model"
"arimelody-web/model/app"
+ "arimelody-web/view"
+
accountRepo "arimelody-web/repository/account"
logRepo "arimelody-web/repository/log"
+ musicRepo "arimelody-web/repository/music"
+
repo "arimelody-web/repository/postgres"
accountService "arimelody-web/service/account"
logService "arimelody-web/service/log"
- "arimelody-web/view"
+ musicService "arimelody-web/service/music"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
@@ -47,6 +51,7 @@ var publicFS embed.FS
func main() {
// 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.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),
)
+ musicRepo := musicRepo.NewMusicRepositoryPostgres(psqlDB)
+ app.MusicService = musicService.NewMusicService(
+ musicRepo,
+ log.New(os.Stderr, "music-repo", model.DEFAULT_LOG_FLAGS),
+ )
+
// handle command arguments
if len(os.Args) > 1 {
arg := os.Args[1]
diff --git a/model/account.go b/model/account.go
index 67424b7..80e0a92 100644
--- a/model/account.go
+++ b/model/account.go
@@ -16,8 +16,8 @@ type (
Email sql.NullString `json:"email" db:"email"`
AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
- FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
- Locked bool `json:"locked" db:"locked"`
+ FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
+ Locked bool `json:"locked" db:"locked"`
Privileges []AccountPrivilege `json:"privileges"`
}
diff --git a/model/app/appstate.go b/model/app/appstate.go
index 2e0c334..80a84d4 100644
--- a/model/app/appstate.go
+++ b/model/app/appstate.go
@@ -8,12 +8,10 @@ import (
"arimelody-web/model/twitch"
logService "arimelody-web/service/log"
- inviteService "arimelody-web/service/invite"
+ //inviteService "arimelody-web/service/invite"
accountService "arimelody-web/service/account"
- sessionService "arimelody-web/service/session"
- artistService "arimelody-web/service/artist"
- releaseService "arimelody-web/service/release"
- trackService "arimelody-web/service/track"
+ //sessionService "arimelody-web/service/session"
+ musicService "arimelody-web/service/music"
)
type (
@@ -55,11 +53,9 @@ type (
PublicFS embed.FS
LogService *logService.LogService
- InviteService *inviteService.InviteService
+ //InviteService *inviteService.InviteService
AccountService *accountService.AccountService
- SesisonService *sessionService.SessionService
- ArtistService *artistService.ArtistService
- ReleaseService *releaseService.ReleaseService
- TrackService *trackService.TrackService
+ //SesisonService *sessionService.SessionService
+ MusicService *musicService.MusicService
}
)
diff --git a/model/artist.go b/model/artist.go
index 746a7dd..b9f940b 100644
--- a/model/artist.go
+++ b/model/artist.go
@@ -9,9 +9,11 @@ type (
}
)
+const DEFAULT_AVATAR_URL = "/img/default-avatar.png"
+
func (artist Artist) GetAvatar() string {
if artist.Avatar == "" {
- return "/img/default-avatar.png"
+ return DEFAULT_AVATAR_URL
}
return artist.Avatar
}
diff --git a/model/artist_test.go b/model/artist_test.go
index feb9a18..8e39479 100644
--- a/model/artist_test.go
+++ b/model/artist_test.go
@@ -1,21 +1,22 @@
-package model
+package model_test
import (
- "testing"
+ "arimelody-web/model"
+ "testing"
)
func Test_Artist_GetAvatar(t *testing.T) {
want := "testavatar.png"
- artist := Artist{ Avatar: want }
+ artist := model.Artist{ Avatar: want }
got := artist.GetAvatar()
if 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()
if want != got {
t.Errorf(`default value not returned when avatar is empty (want "%s", got "%s")`, want, got)
diff --git a/model/credit.go b/model/credit.go
index 2080d83..f5f8feb 100644
--- a/model/credit.go
+++ b/model/credit.go
@@ -2,9 +2,9 @@ package model
type (
Credit struct {
- Release Release `json:"release"`
- Artist Artist `json:"artist"`
- Role string `json:"role"`
- Primary bool `json:"primary" db:"is_primary"`
+ Release *Release `json:"release"`
+ Artist *Artist `json:"artist"`
+ Role string `json:"role"`
+ Primary bool `json:"primary" db:"is_primary"`
}
)
diff --git a/model/link_test.go b/model/link_test.go
index b368094..2458dac 100644
--- a/model/link_test.go
+++ b/model/link_test.go
@@ -1,11 +1,12 @@
-package model
+package model_test
import (
- "testing"
+ "arimelody-web/model"
+ "testing"
)
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",
}
diff --git a/model/release.go b/model/release.go
index 46f4460..3f00d4d 100644
--- a/model/release.go
+++ b/model/release.go
@@ -34,6 +34,8 @@ const (
EP ReleaseType = "EP"
Compilation ReleaseType = "compilation"
Upcoming ReleaseType = "upcoming"
+
+ DEFAULT_RELEASE_ARTWORK_URL = "/img/default-cover-art.png"
)
// GETTERS
@@ -52,7 +54,7 @@ func (release Release) PrintReleaseDate() string {
func (release Release) GetArtwork() string {
if release.Artwork == "" {
- return "/img/default-cover-art.png"
+ return DEFAULT_RELEASE_ARTWORK_URL
}
return release.Artwork
}
@@ -93,3 +95,20 @@ func (release Release) PrintArtists(only_primary bool, ampersand bool) string {
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
+ }
+}
diff --git a/model/release_test.go b/model/release_test.go
index fc0e221..37ef775 100644
--- a/model/release_test.go
+++ b/model/release_test.go
@@ -1,6 +1,7 @@
-package model
+package model_test
import (
+ "arimelody-web/model"
"strings"
"testing"
"time"
@@ -10,7 +11,7 @@ import (
func Test_Release(t *testing.T) {
t.Run("prints correct description HTML", func(t *testing.T) {
- release := Release{
+ release := model.Release{
Description: "this is\na test\ndescription!",
}
@@ -24,7 +25,7 @@ func Test_Release(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),
}
@@ -34,37 +35,37 @@ func Test_Release(t *testing.T) {
t.Run("returns correct artwork", func(t *testing.T) {
artwork := "testartwork.png"
- release := Release{ Artwork: artwork }
+ release := model.Release{ Artwork: artwork }
assert.Equal(t, release.GetArtwork(), artwork)
})
t.Run("returns placeholder artwork when empty", func(t *testing.T) {
- release := Release{}
- assert.Equal(t, release.GetArtwork(), "/img/default-cover-art.png")
+ release := model.Release{}
+ assert.Equal(t, release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
})
t.Run("singles", func(t *testing.T) {
- release := Release{
- Tracks: []*Track{},
+ release := model.Release{
+ Tracks: []*model.Track{},
}
t.Run("false when no tracks are present", func(t *testing.T) {
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) {
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) {
assert.Equal(t, release.IsSingle(), false)
})
})
t.Run("released", func(t *testing.T) {
- release := Release {
+ release := model.Release {
ReleaseDate: time.Now(),
}
@@ -84,23 +85,23 @@ func Test_Release(t *testing.T) {
artist3 := "idk"
artist4 := "guest"
- release := Release{}
+ release := model.Release{}
t.Run("prints \"Unknown Artist\" when release has no credits", func(t *testing.T) {
assert.Equal(t, release.PrintArtists(false, true), "Unknown Artist")
})
release.Credits = append(
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) {
assert.Equal(t, release.PrintArtists(false, true), artist1)
})
- release.Credits = append(release.Credits, []*Credit{
- { Artist: Artist{ Name: artist2 }, Primary: true },
- { Artist: Artist{ Name: artist3 }, Primary: false },
- { Artist: Artist{ Name: artist4 }, Primary: true },
+ release.Credits = append(release.Credits, []*model.Credit{
+ { Artist: &model.Artist{ Name: artist2 }, Primary: true },
+ { Artist: &model.Artist{ Name: artist3 }, Primary: false },
+ { Artist: &model.Artist{ Name: artist4 }, Primary: true },
}...)
t.Run("can get only unique primary artist names", func(t *testing.T) {
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), "")
+ })
+ })
}
diff --git a/model/track_test.go b/model/track_test.go
index eb64b10..091deff 100644
--- a/model/track_test.go
+++ b/model/track_test.go
@@ -1,11 +1,12 @@
-package model
+package model_test
import (
- "testing"
+ "arimelody-web/model"
+ "testing"
)
func Test_Track_DescriptionHTML(t *testing.T) {
- track := Track{
+ track := model.Track{
Description: "this is\na test\ndescription!",
}
@@ -19,7 +20,7 @@ func Test_Track_DescriptionHTML(t *testing.T) {
}
func Test_Track_LyricsHTML(t *testing.T) {
- track := Track{
+ track := model.Track{
Lyrics: "these are\ntest\nlyrics!",
}
diff --git a/repository/account/interface.go b/repository/account/interface.go
index fa31064..ba90073 100644
--- a/repository/account/interface.go
+++ b/repository/account/interface.go
@@ -27,11 +27,11 @@ type AccountRepository interface {
UpdateAvatarURL(id string, avatarURL string) error
RemoveAvatar(id string) error
- Delete(id string) error
-
// Increment the number of account login failure attempts,
// returning the current fail count.
IncrementFails(id string) (int, error)
ResetFails(id string) error
SetLocked(id string, lock bool) error
+
+ Delete(id string) (string, error)
}
diff --git a/repository/account/memory.go b/repository/account/memory.go
index 707da81..7f886d1 100644
--- a/repository/account/memory.go
+++ b/repository/account/memory.go
@@ -1,9 +1,10 @@
package account
import (
+ "arimelody-web/errors"
"arimelody-web/model"
"database/sql"
- "errors"
+ "slices"
"strconv"
)
@@ -24,10 +25,11 @@ func (repo *AccountRepositoryMemory) GetCount() (int, error) {
return len(repo.accounts), nil
}
func (repo *AccountRepositoryMemory) GetByID(id string) (*model.Account, error) {
- for _, account := range repo.accounts {
- if account.ID == id { return account, nil }
- }
- return nil, nil
+ index := slices.IndexFunc(repo.accounts, func(account *model.Account) bool {
+ return account.ID == id
+ })
+ if index == -1 { return nil, nil }
+ return repo.accounts[index], nil
}
func (repo *AccountRepositoryMemory) GetByUsername(username string) (*model.Account, error) {
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.
func (repo *AccountRepositoryMemory) Create(username string, password string, email *string, avatarURL *string) (string, error) {
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 {
- return "", errors.New("Account with this username already exists")
+ return "", errors.NewNotExistError("Account with this username already exists")
}
emailRef := ""
@@ -74,9 +76,9 @@ func (repo *AccountRepositoryMemory) Create(username string, password string, em
// more specialised Update* and Remove* functions should be used.
func (repo *AccountRepositoryMemory) Update(id string, username string, password string, email *string, avatarUrl *string) error {
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 {
- return errors.New("Account with this username already exists")
+ return errors.NewNotExistError("Account with this username already exists")
}
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 {
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 {
- return errors.New("Account with this username already exists")
+ return errors.NewNotExistError("Account with this username already exists")
}
account, err := repo.GetByID(id)
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
return nil
@@ -108,7 +110,7 @@ func (repo *AccountRepositoryMemory) UpdateUsername(id string, username string)
func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string) error {
account, err := repo.GetByID(id)
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
return nil
@@ -116,7 +118,7 @@ func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string)
func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error {
account, err := repo.GetByID(id)
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.String = email
@@ -125,7 +127,7 @@ func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error
func (repo *AccountRepositoryMemory) RemoveEmail(id string) error {
account, err := repo.GetByID(id)
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.String = ""
@@ -134,7 +136,7 @@ func (repo *AccountRepositoryMemory) RemoveEmail(id string) error {
func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string) error {
account, err := repo.GetByID(id)
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.String = avatarURL
@@ -143,31 +145,13 @@ func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string
func (repo *AccountRepositoryMemory) RemoveAvatar(id string) error {
account, err := repo.GetByID(id)
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.String = ""
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,
// returning the current fail count.
func (repo *AccountRepositoryMemory) IncrementFails(id string) (int, error) {
@@ -188,3 +172,17 @@ func (repo *AccountRepositoryMemory) SetLocked(id string, locked bool) error {
account.Locked = locked
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
+}
diff --git a/repository/account/postgres.go b/repository/account/postgres.go
index cad3c61..03d797f 100644
--- a/repository/account/postgres.go
+++ b/repository/account/postgres.go
@@ -157,11 +157,6 @@ func (repo *AccountRepositoryPostgres) RemoveAvatar(id string) error {
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,
// returning the current fail count.
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)
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
+}
diff --git a/repository/music/artist_memory.go b/repository/music/artist_memory.go
new file mode 100644
index 0000000..ff61eee
--- /dev/null
+++ b/repository/music/artist_memory.go
@@ -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
+}
diff --git a/controller/artist.go b/repository/music/artist_postgres.go
similarity index 50%
rename from controller/artist.go
rename to repository/music/artist_postgres.go
index adcdbc5..aa86da9 100644
--- a/controller/artist.go
+++ b/repository/music/artist_postgres.go
@@ -1,17 +1,29 @@
-package controller
+package music
import (
"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{}
- 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 {
return nil, err
}
@@ -19,26 +31,10 @@ func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) {
return &artist, nil
}
-func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) {
+func (repo *MusicRepositoryPostgres) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
var artists = []*model.Artist{}
- err := db.Select(&artists, "SELECT * FROM artist")
- 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,
+ err := repo.db.Select(&artists,
"SELECT * FROM artist "+
"WHERE id NOT IN "+
"(SELECT artist FROM musiccredit WHERE release=$1)",
@@ -50,15 +46,15 @@ func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, err
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 "+
"FROM musiccredit "+
"JOIN musicrelease AS release ON release=release.id "+
"JOIN artist ON artist=artist.id "+
"WHERE artist=$1 "
- if !show_hidden { query += "AND visible=true " }
+ if !showHidden { query += "AND visible=true " }
query += "ORDER BY release_date DESC"
- rows, err := db.Query(query, artistID)
+ rows, err := repo.db.Query(query, artistID)
if err != nil {
return nil, err
}
@@ -70,7 +66,10 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.
}
var credits []*model.Credit
for rows.Next() {
- var credit model.Credit
+ credit := &model.Credit{
+ Release: &model.Release{},
+ Artist: &model.Artist{},
+ }
err = rows.Scan(
&credit.Release.ID,
&credit.Release.Title,
@@ -85,44 +84,42 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.
)
otherArtists := []NamePrimary{}
- err = db.Select(&otherArtists,
+ err = repo.db.Select(&otherArtists,
"SELECT name,is_primary FROM artist "+
"JOIN musiccredit ON artist=id "+
"WHERE release=$1",
credit.Release.ID)
for _, otherCredit := range otherArtists {
credit.Release.Credits = append(credit.Release.Credits, &model.Credit{
- Artist: model.Artist{
+ Artist: &model.Artist{
Name: otherCredit.Name,
},
Primary: otherCredit.Primary,
})
}
- credits = append(credits, &credit)
+ credits = append(credits, credit)
}
return credits, nil
}
-func CreateArtist(db *sqlx.DB, artist *model.Artist) error {
- _, err := db.Exec(
+func (repo *MusicRepositoryPostgres) CreateArtist(
+ id string,
+ name string,
+ website string,
+ avatar string,
+) error {
+ _, err := repo.db.Exec(
"INSERT INTO artist (id, name, website, avatar) "+
"VALUES ($1, $2, $3, $4)",
- artist.ID,
- artist.Name,
- artist.Website,
- artist.Avatar,
+ id, name, website, avatar,
)
- if err != nil {
- return err
- }
-
- return nil
+ return err
}
-func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
- _, err := db.Exec(
+func (repo *MusicRepositoryPostgres) UpdateArtist(artist *model.Artist) error {
+ _, err := repo.db.Exec(
"UPDATE artist "+
"SET name=$2, website=$3, avatar=$4 "+
"WHERE id=$1",
@@ -131,22 +128,27 @@ func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
artist.Website,
artist.Avatar,
)
- if err != nil {
- return err
- }
-
- return nil
+ return err
+}
+func (repo *MusicRepositoryPostgres) UpdateArtistID(oldID string, newID string) error {
+ _, 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 {
- _, err := db.Exec(
- "DELETE FROM artist "+
- "WHERE id=$1",
- artistID,
- )
- if err != nil {
- return err
- }
-
- return nil
+func (repo *MusicRepositoryPostgres) DeleteArtist(id string) (string, error) {
+ var deletedID string
+ err := repo.db.Get(&deletedID, "DELETE FROM artist WHERE id=$1", id)
+ return deletedID, err
}
diff --git a/repository/music/interface.go b/repository/music/interface.go
new file mode 100644
index 0000000..5fdc3ac
--- /dev/null
+++ b/repository/music/interface.go
@@ -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,
+ }
+}
diff --git a/repository/music/release_memory.go b/repository/music/release_memory.go
new file mode 100644
index 0000000..3e99612
--- /dev/null
+++ b/repository/music/release_memory.go
@@ -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
+}
diff --git a/repository/music/release_postgres.go b/repository/music/release_postgres.go
new file mode 100644
index 0000000..e98a165
--- /dev/null
+++ b/repository/music/release_postgres.go
@@ -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
+}
diff --git a/repository/music/track_memory.go b/repository/music/track_memory.go
new file mode 100644
index 0000000..7ad574b
--- /dev/null
+++ b/repository/music/track_memory.go
@@ -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
+}
diff --git a/repository/music/track_postgres.go b/repository/music/track_postgres.go
new file mode 100644
index 0000000..b0b0946
--- /dev/null
+++ b/repository/music/track_postgres.go
@@ -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
+}
diff --git a/service/account/account.go b/service/account/account.go
index 658252f..bf7afd4 100644
--- a/service/account/account.go
+++ b/service/account/account.go
@@ -1,10 +1,10 @@
package account
import (
+ "arimelody-web/errors"
"arimelody-web/model"
repository "arimelody-web/repository/account"
- "errors"
- "fmt"
+ "arimelody-web/service/validator"
"log"
)
@@ -29,33 +29,24 @@ func (s *AccountService) GetCount() (int, error) {
}
func (s *AccountService) GetByID(id string) (*model.Account, error) {
- if account, err := s.repo.GetByID(id); err != nil {
- return nil, err
- } else if account == nil {
- return nil, fmt.Errorf("Account does not exist: %s", id)
- } else {
- return account, nil
- }
+ account, err := s.repo.GetByID(id)
+ if err != nil { return nil, err }
+ if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
+ return account, nil
}
func (s *AccountService) GetByUsername(username string) (*model.Account, error) {
- if account, err := s.repo.GetByUsername(username); err != nil {
- return nil, err
- } else if account == nil {
- return nil, fmt.Errorf("Account does not exist: %s", username)
- } else {
- return account, nil
- }
+ account, err := s.repo.GetByUsername(username)
+ if err != nil { return nil, err }
+ if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
+ return account, nil
}
func (s *AccountService) GetByEmail(email string) (*model.Account, error) {
- if account, err := s.repo.GetByEmail(email); err != nil {
- return nil, err
- } else if account == nil {
- return nil, fmt.Errorf("Account does not exist with email: %s", email)
- } else {
- return account, nil
- }
+ account, err := s.repo.GetByEmail(email)
+ if err != nil { return nil, err }
+ if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
+ return account, nil
}
func (s *AccountService) Create(
@@ -64,13 +55,13 @@ func (s *AccountService) Create(
email *string,
avatarURL *string,
) (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 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 {
return id, err
}
@@ -80,48 +71,31 @@ func (s *AccountService) Create(
}
func (s *AccountService) ChangeUsername(id string, username string) error {
- if len(username) == 0 { return errors.New("Username cannot be empty") }
- if err := s.repo.UpdateAvatarURL(id, username); err != nil {
- return err
- }
-
+ if len(username) == 0 { return errors.NewValidationError("Username cannot be empty") }
+ if !validator.ValidateID(username) { return errors.NewValidationError("Username contains invalid characters") }
+ if err := s.repo.UpdateUsername(id, username); err != nil { return err }
s.log.Printf("Changed username for %s to '%s'", id, username)
return nil
}
func (s *AccountService) ChangePassword(id string, password string) error {
- if len(password) == 0 { return errors.New("Password cannot be empty") }
- if err := s.repo.UpdateAvatarURL(id, password); err != nil {
- return err
- }
-
+ if len(password) == 0 { return errors.NewValidationError("Password cannot be empty") }
+ if err := s.repo.UpdatePassword(id, password); err != nil { return err }
s.log.Printf("Changed password for %s", id)
return nil
}
func (s *AccountService) ChangeEmail(id string, email string) error {
if len(email) == 0 { return s.repo.RemoveEmail(id) }
- if err := s.repo.UpdateAvatarURL(id, email); err != nil {
- return err
- }
-
+ if err := s.repo.UpdateEmail(id, email); err != nil { return err }
s.log.Printf("Changed email for %s to '%s'", id, email)
return nil
}
func (s *AccountService) ChangeAvatarURL(id string, avatarURL string) error {
if len(avatarURL) == 0 { return s.repo.RemoveAvatar(id) }
- if err := s.repo.UpdateAvatarURL(id, avatarURL); err != nil {
- return err
- }
-
+ if err := s.repo.UpdateAvatarURL(id, avatarURL); err != nil { return err }
s.log.Printf("Changed avatar URL for %s to '%s'", id, avatarURL)
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) {
num, err := s.repo.IncrementFails(id)
if err != nil { return 0, err }
@@ -146,3 +120,11 @@ func (s *AccountService) Unlock(id string) error {
s.log.Printf("Unlocked account %s", id)
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
+}
diff --git a/service/account/account_test.go b/service/account/account_test.go
index 0c9a372..5eec680 100644
--- a/service/account/account_test.go
+++ b/service/account/account_test.go
@@ -1,16 +1,19 @@
-package account
+package account_test
import (
"arimelody-web/model"
repository "arimelody-web/repository/account"
+ service "arimelody-web/service/account"
+ "arimelody-web/errors"
"log"
"os"
"testing"
- "gotest.tools/v3/assert"
+
+ "gotest.tools/v3/assert"
)
var (
- service *AccountService
+ s *service.AccountService
)
func init() {
@@ -19,7 +22,7 @@ func init() {
defer devNullFile.Close()
repo := repository.NewAccountRepositoryMemory(make([]*model.Account, 0))
- service = NewAccountService(
+ s = service.NewAccountService(
repo,
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
)
@@ -41,14 +44,14 @@ func Test_Account(t *testing.T) {
username := "testificate"
password := "the amazing digital data breach"
email := "goober@arimelody.space"
- avatarURL := "/img/default-avatar.webp"
+ avatarURL := "/img/account-avatar.webp"
var id string
var err error
t.Run("accounts should start empty", 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)
} else {
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) {
- if accounts, err := service.GetAll(); err != nil {
- t.Errorf("Failed to get number of accounts: %v", err)
+ if accounts, err := s.GetAll(); err != nil {
+ t.Errorf("Failed to get accounts: %v", err)
} else {
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) {
- id, err = service.Create(username, password, &email, &avatarURL)
+ id, err = s.Create(username, password, &email, &avatarURL)
if err != nil {
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) {
- account, err := service.GetByID(id)
+ account, err := s.GetByID(id)
if err != nil {
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)
})
t.Run("and fetch by username", func(t *testing.T) {
- account, err := service.GetByUsername(username)
+ account, err := s.GetByUsername(username)
if err != nil {
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)
})
t.Run("and fetch by email", func(t *testing.T) {
- account, err := service.GetByEmail(email)
+ account, err := s.GetByEmail(email)
if err != nil {
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("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)
} else {
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) {
- if accounts, err := service.GetAll(); err != nil {
- t.Errorf("Failed to get number of accounts: %v", err)
+ if accounts, err := s.GetAll(); err != nil {
+ t.Errorf("Failed to get accounts: %v", err)
} else {
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) {
- _, err := service.Create(username, password, &email, &avatarURL)
+ _, err := s.Create(username, password, &email, &avatarURL)
if err == nil {
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) {
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)
}
- if account, err := service.GetByID(id); err != nil {
+ if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err)
} else if account == nil {
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) {
- if err := service.ChangeUsername(id, ""); err == nil {
+ if err := s.ChangeUsername(id, ""); err == nil {
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) {
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)
}
- if account, err := service.GetByID(id); err != nil {
+ if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err)
} else if account == nil {
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) {
- if err := service.ChangePassword(id, ""); err == nil {
+ if err := s.ChangePassword(id, ""); err == nil {
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) {
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)
}
- if account, err := service.GetByID(id); err != nil {
+ if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err)
} else if account == nil {
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) {
- if err := service.ChangeEmail(id, ""); err != nil {
+ if err := s.ChangeEmail(id, ""); err != nil {
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)
} else if account == nil {
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) {
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)
}
- if account, err := service.GetByID(id); err != nil {
+ if account, err := s.GetByID(id); err != nil {
t.Errorf("Failed to get account: %v", err)
} else if account == nil {
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) {
- if err := service.ChangeAvatarURL(id, ""); err != nil {
+ if err := s.ChangeAvatarURL(id, ""); err != nil {
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)
} else if account == nil {
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) {
- 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)
} else {
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)
} else if account == nil {
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) {
- if err := service.ResetFails(id); err != nil {
+ if err := s.ResetFails(id); err != nil {
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)
} else if account == nil {
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) {
- if err := service.Lock(id); err != nil {
+ if err := s.Lock(id); err != nil {
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)
} else if account == nil {
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) {
- if err := service.Unlock(id); err != nil {
+ if err := s.Unlock(id); err != nil {
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)
} else if account == nil {
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) {
- if err = service.Delete(id); err != nil {
+ if err = s.Delete(id); err != nil {
t.Errorf("Failed to delete account: %v", err)
}
- if account, err := service.GetByID(id); err != nil {
- t.Errorf("Failed to get account after deletion: %v", err)
+ if account, err := s.GetByID(id); err != nil {
+ if !errors.IsNotExistError(err) {
+ t.Errorf("Failed to get account after deletion: %v", err)
+ }
} else if account != nil {
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("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.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.Run("email", func(t *testing.T) {
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.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) {
garbageAccountID := "adsginh534g9405gmb40i9bm"
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.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.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.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.Cleanup(func() {
- if err := service.Delete(id); err != nil {
- t.Errorf("Failed to clean up test case: %v", err)
- }
- })
}
diff --git a/service/log/log.go b/service/log/log.go
index 21f1650..bf06b7b 100644
--- a/service/log/log.go
+++ b/service/log/log.go
@@ -3,6 +3,7 @@ package log
import (
"arimelody-web/model"
repository "arimelody-web/repository/log"
+ "arimelody-web/errors"
"fmt"
"log"
"time"
@@ -43,7 +44,10 @@ func (s *LogService) Warn(logType string, format string, args ...any) {
}
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(
diff --git a/service/music/artist.go b/service/music/artist.go
new file mode 100644
index 0000000..aa643af
--- /dev/null
+++ b/service/music/artist.go
@@ -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
+}
diff --git a/service/music/artist_test.go b/service/music/artist_test.go
new file mode 100644
index 0000000..1a59645
--- /dev/null
+++ b/service/music/artist_test.go
@@ -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)
+ }
+ }
+ })
+ })
+}
diff --git a/service/music/music.go b/service/music/music.go
new file mode 100644
index 0000000..f3037cc
--- /dev/null
+++ b/service/music/music.go
@@ -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,
+ }
+}
diff --git a/service/music/music_test.go b/service/music/music_test.go
new file mode 100644
index 0000000..72447a4
--- /dev/null
+++ b/service/music/music_test.go
@@ -0,0 +1,9 @@
+package music_test
+
+import (
+ service "arimelody-web/service/music"
+)
+
+var (
+ s *service.MusicService
+)
diff --git a/service/music/release.go b/service/music/release.go
new file mode 100644
index 0000000..494477a
--- /dev/null
+++ b/service/music/release.go
@@ -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
+}
diff --git a/service/music/release_test.go b/service/music/release_test.go
new file mode 100644
index 0000000..d14a697
--- /dev/null
+++ b/service/music/release_test.go
@@ -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)
+ }
+ }
+ })
+ })
+}
diff --git a/service/music/track.go b/service/music/track.go
new file mode 100644
index 0000000..5a9e993
--- /dev/null
+++ b/service/music/track.go
@@ -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
+}
diff --git a/service/validator/validator.go b/service/validator/validator.go
new file mode 100644
index 0000000..cb23857
--- /dev/null
+++ b/service/validator/validator.go
@@ -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)
+}
diff --git a/service/validator/validator_test.go b/service/validator/validator_test.go
new file mode 100644
index 0000000..0de2353
--- /dev/null
+++ b/service/validator/validator_test.go
@@ -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)
+ })
+ })
+}
diff --git a/view/music.go b/view/music.go
index 9cb4e59..ed48c1b 100644
--- a/view/music.go
+++ b/view/music.go
@@ -22,7 +22,7 @@ func MusicHandler(app *app.AppState) http.Handler {
return
}
- release, err := controller.GetRelease(app.DB, r.URL.Path[1:], true)
+ release, err := app.MusicService.GetFullReleaseByID(r.URL.Path[1:])
if err != nil {
http.NotFound(w, r)
return
@@ -36,7 +36,7 @@ func MusicHandler(app *app.AppState) http.Handler {
func ServeCatalog(app *app.AppState) http.Handler {
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 {
fmt.Printf("WARN: Failed to pull releases for catalog: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)