diff --git a/admin/accounthttp.go b/admin/accounthttp.go index d26e9bf..c93c8b9 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -115,7 +115,7 @@ func changePasswordHandler(app *app.AppState) http.Handler { return } - app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) + app.Log.Info(model.LOG_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, "Password updated successfully.") @@ -145,7 +145,7 @@ func deleteAccountHandler(app *app.AppState) http.Handler { // check password if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil { - app.LogService.Warn(model.LOG_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r)) + app.Log.Warn(model.LOG_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r)) controller.SetSessionError(app.DB, session, "Incorrect password.") http.Redirect(w, r, "/admin/account", http.StatusFound) return @@ -159,7 +159,7 @@ func deleteAccountHandler(app *app.AppState) http.Handler { return } - app.LogService.Info(model.LOG_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) + app.Log.Info(model.LOG_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) controller.SetSessionAccount(app.DB, session, nil) controller.SetSessionError(app.DB, session, "") @@ -310,7 +310,7 @@ func totpConfirmHandler(app *app.AppState) http.Handler { return } - app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name) + app.Log.Info(model.LOG_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name) controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name)) @@ -358,7 +358,7 @@ func totpDeleteHandler(app *app.AppState) http.Handler { return } - app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name) + app.Log.Info(model.LOG_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name) controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name)) diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 05cfbb5..ef67a4e 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -6,6 +6,7 @@ import ( "strings" "arimelody-web/admin/templates" + "arimelody-web/controller" "arimelody-web/model" "arimelody-web/model/app" ) @@ -22,7 +23,7 @@ func serveArtists(app *app.AppState) http.Handler { return } - artists, err := app.MusicService.GetAllArtists() + artists, err := controller.GetAllArtists(app.DB) if err != nil { fmt.Printf("WARN: Failed to fetch artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -49,7 +50,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 := app.MusicService.GetArtistByID(artistID) + artist, err := controller.GetArtist(app.DB, artistID) if err != nil { if artist == nil { http.NotFound(w, r) @@ -60,7 +61,7 @@ func serveArtist(app *app.AppState, artistID string) http.Handler { return } - credits, err := app.MusicService.GetArtistCredits(artistID, true) + credits, err := controller.GetArtistCredits(app.DB, artist.ID, 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 66e66f1..10dc3f6 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 := app.MusicService.GetAllReleases(false, 3) + releases, err := controller.GetAllReleases(app.DB, false, 3, true) 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 := app.MusicService.GetReleaseCount(false) + releaseCount, err := controller.GetReleaseCount(app.DB, 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 := app.MusicService.GetAllArtists() + artists, err := controller.GetAllArtists(app.DB) 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 := app.MusicService.GetArtistCount() + artistCount, err := controller.GetArtistCount(app.DB) 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 := app.MusicService.GetOrphanTracks() + tracks, err := controller.GetOrphanTracks(app.DB) 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 := app.MusicService.GetTrackCount() + trackCount, err := controller.GetTrackCount(app.DB) 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) @@ -241,7 +241,7 @@ func registerAccountHandler(app *app.AppState) http.Handler { return } - app.LogService.Info( + app.Log.Info( model.LOG_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", credentials.Username, @@ -252,7 +252,7 @@ func registerAccountHandler(app *app.AppState) http.Handler { err = controller.DeleteInvite(app.DB, invite.Code) if err != nil { - app.LogService.Warn(model.LOG_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err) + app.Log.Warn(model.LOG_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err) } // registration success! @@ -332,7 +332,7 @@ func loginHandler(app *app.AppState) http.Handler { err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) if err != nil { - app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r)) + app.Log.Warn(model.LOG_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r)) if locked := handleFailedLogin(app, account, r); locked { controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") } else { @@ -366,8 +366,8 @@ func loginHandler(app *app.AppState) http.Handler { // login success! // TODO: log login activity to user - app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r)) - app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username) + app.Log.Info(model.LOG_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r)) + app.Log.Warn(model.LOG_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username) err = controller.SetSessionAccount(app.DB, session, account) if err != nil { @@ -420,7 +420,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler { totpCode := r.FormValue("totp") if len(totpCode) != controller.TOTP_CODE_LENGTH { - app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) + app.Log.Warn(model.LOG_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) controller.SetSessionError(app.DB, session, "Invalid TOTP.") render() return @@ -434,7 +434,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler { return } if totpMethod == nil { - app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) + app.Log.Warn(model.LOG_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) if locked := handleFailedLogin(app, session.AttemptAccount, r); locked { controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") controller.SetSessionAttemptAccount(app.DB, session, nil) @@ -446,7 +446,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler { return } - app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r)) + app.Log.Info(model.LOG_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r)) err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount) if err != nil { @@ -574,7 +574,7 @@ func handleFailedLogin(app *app.AppState, account *model.Account, r *http.Reques account.Username, err, ) - app.LogService.Warn( + app.Log.Warn( model.LOG_ACCOUNT, "Failed to lock account \"%s\"", account.Username, @@ -588,7 +588,7 @@ func handleFailedLogin(app *app.AppState, account *model.Account, r *http.Reques model.MAX_LOGIN_FAIL_ATTEMPTS, controller.ResolveIP(app, r), ) - app.LogService.Warn( + app.Log.Warn( model.LOG_ACCOUNT, "Account \"%s\" was locked: %d failed login attempts (IP: %s)", account.Username, @@ -606,7 +606,7 @@ func handleFailedLogin(app *app.AppState, account *model.Account, r *http.Reques account.Username, err, ) - app.LogService.Warn( + app.Log.Warn( model.LOG_ACCOUNT, "Failed to increment login failures for \"%s\"", account.Username, diff --git a/admin/logshttp.go b/admin/logshttp.go index b17095d..70c9627 100644 --- a/admin/logshttp.go +++ b/admin/logshttp.go @@ -43,7 +43,7 @@ func logsHandler(app *app.AppState) http.Handler { } } - logs, err := app.LogService.Search(levelFilter, typeFilter, query, 100, 0) + logs, err := app.Log.Search(levelFilter, typeFilter, query, 100, 0) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch audit logs: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/releasehttp.go b/admin/releasehttp.go index 72586ed..d16528e 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 := app.MusicService.GetAllFullReleases(false, 0) + releases, err := controller.GetAllReleases(app.DB, false, 0, true) 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,13 +60,9 @@ 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 := app.MusicService.GetFullReleaseByID(releaseID) + release, err := controller.GetRelease(app.DB, releaseID, true) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -134,7 +130,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 := app.MusicService.GetArtistsNotOnRelease(release.ID) + artists, err := controller.GetArtistsNotOnRelease(app.DB, 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) @@ -162,7 +158,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 := app.MusicService.GetArtistByID(artistID) + artist, err := controller.GetArtist(app.DB, 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) @@ -211,7 +207,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 := app.MusicService.GetTracksNotOnRelease(release.ID) + tracks, err := controller.GetTracksNotOnRelease(app.DB, 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) @@ -239,7 +235,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 := app.MusicService.GetTrackByID(trackID) + track, err := controller.GetTrack(app.DB, 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 8c4554b..2122748 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -6,6 +6,7 @@ import ( "strings" "arimelody-web/admin/templates" + "arimelody-web/controller" "arimelody-web/model" "arimelody-web/model/app" ) @@ -22,7 +23,7 @@ func serveTracks(app *app.AppState) http.Handler { return } - tracks, err := app.MusicService.GetAllTracks() + tracks, err := controller.GetAllTracks(app.DB) if err != nil { fmt.Printf("WARN: Failed to fetch tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -49,7 +50,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 := app.MusicService.GetTrackByID(trackID) + track, err := controller.GetTrack(app.DB, 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) @@ -60,7 +61,7 @@ func serveTrack(app *app.AppState, trackID string) http.Handler { return } - releases, err := app.MusicService.GetTrackReleases(trackID) + releases, err := controller.GetTrackReleases(app.DB, track.ID, true) 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 f23fcbd..84b7f8a 100644 --- a/api/api.go +++ b/api/api.go @@ -10,7 +10,6 @@ import ( "arimelody-web/controller" "arimelody-web/model" "arimelody-web/model/app" - "arimelody-web/errors" ) func Handler(app *app.AppState) http.Handler { @@ -22,9 +21,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 := app.MusicService.GetArtistByID(artistID) + artist, err := controller.GetArtist(app.DB, artistID) if err != nil { - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -64,9 +63,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 := app.MusicService.GetFullReleaseByID(releaseID) + release, err := controller.GetRelease(app.DB, releaseID, true) if err != nil { - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -106,9 +105,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 := app.MusicService.GetTrackByID(trackID) + track, err := controller.GetTrack(app.DB, trackID) if err != nil { - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -188,10 +187,7 @@ func getSession(app *app.AppState, r *http.Request) (*model.Session, error) { // fetch existing session session, err := controller.GetSession(app, token) - if errors.IsValidationError(err) { - return nil, err - } - if errors.IsNotExistError(err) { + if err != nil && !strings.Contains(err.Error(), "no rows") { return nil, fmt.Errorf("Failed to retrieve session: %v\n", err) } diff --git a/api/artist.go b/api/artist.go index 61cb207..d5f8a31 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 := app.MusicService.GetAllArtists() + artists, err := controller.GetAllArtists(app.DB) 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) - showHiddenReleases := session != nil && session.Account != nil + show_hidden_releases := session != nil && session.Account != nil - dbCredits, err := app.MusicService.GetArtistCredits(artist.ID, showHiddenReleases) + dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases) 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,34 +91,31 @@ func CreateArtist(app *app.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) - type CreateArtistDTO struct { - ID string `json:"id"` - Name string `json:"name"` - } - dto := &CreateArtistDTO{} - - err := json.NewDecoder(r.Body).Decode(dto) + var artist model.Artist + err := json.NewDecoder(r.Body).Decode(&artist) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - err = app.MusicService.CreateArtist(dto.ID, dto.Name, "", "") + 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) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } if strings.Contains(err.Error(), "duplicate key") { - http.Error(w, fmt.Sprintf("Artist %s already exists\n", dto.ID), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) return } - fmt.Printf("WARN: Failed to create artist %s: %s\n", dto.ID, err) + fmt.Printf("WARN: Failed to create artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" created by \"%s\".", dto.Name, session.Account.Username) + app.Log.Info(model.LOG_ARTIST, "Artist \"%s\" created by \"%s\".", artist.Name, session.Account.Username) w.WriteHeader(http.StatusCreated) }) @@ -159,13 +156,9 @@ func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler { } } - err = app.MusicService.UpdateArtist(artist) + err = controller.UpdateArtist(app.DB, artist) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -173,7 +166,7 @@ func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" updated by \"%s\".", artist.Name, session.Account.Username) + app.Log.Info(model.LOG_ARTIST, "Artist \"%s\" updated by \"%s\".", artist.Name, session.Account.Username) }) } @@ -181,13 +174,9 @@ 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 := app.MusicService.DeleteArtist(artist.ID) + err := controller.DeleteArtist(app.DB, artist.ID) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -195,6 +184,6 @@ func DeleteArtist(app *app.AppState, artist *model.Artist) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" deleted by \"%s\".", artist.Name, session.Account.Username) + app.Log.Info(model.LOG_ARTIST, "Artist \"%s\" deleted by \"%s\".", artist.Name, session.Account.Username) }) } diff --git a/api/release.go b/api/release.go index e97eb16..71bac93 100644 --- a/api/release.go +++ b/api/release.go @@ -13,7 +13,6 @@ import ( "arimelody-web/controller" "arimelody-web/model" "arimelody-web/model/app" - "arimelody-web/errors" ) func ServeRelease(app *app.AppState, release *model.Release) http.Handler { @@ -40,50 +39,50 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler { } type ( - TrackDTO struct { + Track struct { Title string `json:"title"` Description string `json:"description"` Lyrics string `json:"lyrics"` } - CreditDTO struct { + Credit struct { *model.Artist Role string `json:"role"` Primary bool `json:"primary"` } - ReleaseDTO struct { + Release struct { *model.Release - Tracks []TrackDTO `json:"tracks"` - Credits []CreditDTO `json:"credits"` + Tracks []Track `json:"tracks"` + Credits []Credit `json:"credits"` Links map[string]string `json:"links"` } ) - response := ReleaseDTO{ + response := Release{ Release: release, - Tracks: []TrackDTO{}, - Credits: []CreditDTO{}, + Tracks: []Track{}, + Credits: []Credit{}, Links: make(map[string]string), } if release.IsReleased() || privileged { // get credits - credits, err := app.MusicService.GetReleaseCredits(release.ID) + credits, err := controller.GetReleaseCredits(app.DB, 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 := app.MusicService.GetArtistByID(credit.Artist.ID) + artist, err := controller.GetArtist(app.DB, 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, CreditDTO{ + response.Credits = append(response.Credits, Credit{ Artist: artist, Role: credit.Role, Primary: credit.Primary, @@ -91,14 +90,14 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler { } // get tracks - tracks, err := app.MusicService.GetReleaseTracks(release.ID) + tracks, err := controller.GetReleaseTracks(app.DB, 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, TrackDTO{ + response.Tracks = append(response.Tracks, Track{ Title: track.Title, Description: track.Description, Lyrics: track.Lyrics, @@ -106,7 +105,7 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler { } // get links - links, err := app.MusicService.GetReleaseLinks(release.ID) + links, err := controller.GetReleaseLinks(app.DB, 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) @@ -130,7 +129,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 := app.MusicService.GetAllFullReleases(false, 0) + releases, err := controller.GetAllReleases(app.DB, false, 0, true) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -193,56 +192,47 @@ func CreateRelease(app *app.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) - 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) + var release model.Release + err := json.NewDecoder(r.Body).Decode(&release) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - 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 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.Artwork == "" { dto.Artwork = model.DEFAULT_RELEASE_ARTWORK_URL } - err = app.MusicService.CreateRelease( - dto.ID, - dto.Title, - dto.ReleaseType, - dto.ReleaseDate, - dto.Artwork, - ) + if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } + + err = controller.CreateRelease(app.DB, &release) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } if strings.Contains(err.Error(), "duplicate key") { - http.Error(w, fmt.Sprintf("Release %s already exists\n", dto.ID), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) return } - fmt.Printf("WARN: Failed to create release %s: %s\n", dto.ID, err) + fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" created by \"%s\".", dto.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Release \"%s\" created by \"%s\".", release.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(dto) + err = encoder.Encode(release) if err != nil { - fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", dto.ID, err) + fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -307,13 +297,9 @@ func UpdateRelease(app *app.AppState, release *model.Release) http.Handler { } } - err = app.MusicService.UpdateRelease(release) + err = controller.UpdateRelease(app.DB, release) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -321,7 +307,7 @@ func UpdateRelease(app *app.AppState, release *model.Release) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) }) } @@ -329,32 +315,28 @@ 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 newTrackIDs = []string{} - err := json.NewDecoder(r.Body).Decode(&newTrackIDs) + var trackIDs = []string{} + err := json.NewDecoder(r.Body).Decode(&trackIDs) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - err = app.MusicService.UpdateReleaseTracks(release.ID, newTrackIDs) + err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - 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 } + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + 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, "Release \"%s\" tracklist updated by \"%s\".", release.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) }) } @@ -362,22 +344,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 CreditDTO struct { + type creditJSON struct { Artist string Role string Primary bool } - var dto []CreditDTO - err := json.NewDecoder(r.Body).Decode(&dto) + var data []creditJSON + err := json.NewDecoder(r.Body).Decode(&data) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - var newCredits []*model.Credit - for _, credit := range dto { - newCredits = append(newCredits, &model.Credit{ - Artist: &model.Artist{ + var credits []*model.Credit + for _, credit := range data { + credits = append(credits, &model.Credit{ + Artist: model.Artist{ ID: credit.Artist, }, Role: credit.Role, @@ -385,25 +367,21 @@ func UpdateReleaseCredits(app *app.AppState, release *model.Release) http.Handle }) } - err = app.MusicService.UpdateReleaseCredits(release.ID, newCredits) + err = controller.UpdateReleaseCredits(app.DB, release.ID, credits) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - 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 } + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + 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, "Release \"%s\" credits updated by \"%s\".", release.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) }) } @@ -411,41 +389,28 @@ 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) - type LinkDTO struct { - Name string `json:"name"` - URL string `json:"url"` - } - var dto = []LinkDTO{} - err := json.NewDecoder(r.Body).Decode(&dto) + var links = []*model.Link{} + err := json.NewDecoder(r.Body).Decode(&links) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - 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) + err = controller.UpdateReleaseLinks(app.DB, release.ID, links) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - 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 } + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + 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, "Release \"%s\" links updated by \"%s\".", release.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) }) } @@ -453,13 +418,9 @@ 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 := app.MusicService.DeleteRelease(release.ID) + err := controller.DeleteRelease(app.DB, release.ID) if err != nil { - if errors.IsValidationError(err) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if errors.IsNotExistError(err) { + if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } @@ -467,6 +428,6 @@ func DeleteRelease(app *app.AppState, release *model.Release) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username) }) } diff --git a/api/track.go b/api/track.go index 749c66e..8d81ea6 100644 --- a/api/track.go +++ b/api/track.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "arimelody-web/errors" + "arimelody-web/controller" "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 := app.MusicService.GetAllTracks() + dbTracks, err := controller.GetAllTracks(app.DB) 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 := app.MusicService.GetTrackReleases(track.ID) + dbReleases, err := controller.GetTrackReleases(app.DB, track.ID, false) 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,30 +78,26 @@ func CreateTrack(app *app.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) - 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) + var track model.Track + err := json.NewDecoder(r.Body).Decode(&track) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - id, err := app.MusicService.CreateTrack(dto.Title, dto.Description, dto.Lyrics, "") + if track.Title == "" { + http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) + return + } + + id, err := controller.CreateTrack(app.DB, &track) 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\".", dto.Title, id, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Track \"%s\" (%s) created by \"%s\".", track.Title, track.ID, session.Account.Username) w.Header().Add("Content-Type", "text/plain") w.WriteHeader(http.StatusCreated) @@ -118,34 +114,25 @@ func UpdateTrack(app *app.AppState, track *model.Track) http.Handler { session := r.Context().Value("session").(*model.Session) - 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) + err := json.NewDecoder(r.Body).Decode(&track) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - track.Title = dto.Title - track.Description = dto.Description - track.Lyrics = dto.Lyrics + if track.Title == "" { + http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) + return + } - err = app.MusicService.UpdateTrack(track) + err = controller.UpdateTrack(app.DB, 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 } - app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) updated by \"%s\".", track.Title, track.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Track \"%s\" (%s) updated by \"%s\".", track.Title, track.ID, session.Account.Username) w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) @@ -167,16 +154,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 := app.MusicService.DeleteTrack(trackID) + err := controller.DeleteTrack(app.DB, 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) } - app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) deleted by \"%s\".", track.Title, track.ID, session.Account.Username) + app.Log.Info(model.LOG_MUSIC, "Track \"%s\" (%s) deleted by \"%s\".", track.Title, track.ID, session.Account.Username) }) } diff --git a/api/uploads.go b/api/uploads.go index fb40f0f..27731c2 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -50,7 +50,7 @@ func HandleImageUpload(app *app.AppState, data *string, directory string, filena return "", nil } - app.LogService.Info(model.LOG_FILES, "\"%s\" created.", imagePath) + app.Log.Info(model.LOG_FILES, "\"%s\" created.", imagePath) return filename, nil } diff --git a/repository/music/artist_postgres.go b/controller/artist.go similarity index 50% rename from repository/music/artist_postgres.go rename to controller/artist.go index aa86da9..adcdbc5 100644 --- a/repository/music/artist_postgres.go +++ b/controller/artist.go @@ -1,29 +1,17 @@ -package music +package controller import ( "arimelody-web/model" + + "github.com/jmoiron/sqlx" ) -func (repo *MusicRepositoryPostgres) GetAllArtists() ([]*model.Artist, error) { - var artists = []*model.Artist{} +// DATABASE - 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) { +func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) { var artist = model.Artist{} - err := repo.db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) + err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) if err != nil { return nil, err } @@ -31,10 +19,26 @@ func (repo *MusicRepositoryPostgres) GetArtistByID(id string) (*model.Artist, er return &artist, nil } -func (repo *MusicRepositoryPostgres) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) { +func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) { var artists = []*model.Artist{} - err := repo.db.Select(&artists, + 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, "SELECT * FROM artist "+ "WHERE id NOT IN "+ "(SELECT artist FROM musiccredit WHERE release=$1)", @@ -46,15 +50,15 @@ func (repo *MusicRepositoryPostgres) GetArtistsNotOnRelease(releaseID string) ([ return artists, nil } -func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) { +func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden 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 !showHidden { query += "AND visible=true " } + if !show_hidden { query += "AND visible=true " } query += "ORDER BY release_date DESC" - rows, err := repo.db.Query(query, artistID) + rows, err := db.Query(query, artistID) if err != nil { return nil, err } @@ -66,10 +70,7 @@ func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidde } var credits []*model.Credit for rows.Next() { - credit := &model.Credit{ - Release: &model.Release{}, - Artist: &model.Artist{}, - } + var credit model.Credit err = rows.Scan( &credit.Release.ID, &credit.Release.Title, @@ -84,42 +85,44 @@ func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidde ) otherArtists := []NamePrimary{} - err = repo.db.Select(&otherArtists, + err = 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 (repo *MusicRepositoryPostgres) CreateArtist( - id string, - name string, - website string, - avatar string, -) error { - _, err := repo.db.Exec( +func CreateArtist(db *sqlx.DB, artist *model.Artist) error { + _, err := db.Exec( "INSERT INTO artist (id, name, website, avatar) "+ "VALUES ($1, $2, $3, $4)", - id, name, website, avatar, + artist.ID, + artist.Name, + artist.Website, + artist.Avatar, ) - return err + if err != nil { + return err + } + + return nil } -func (repo *MusicRepositoryPostgres) UpdateArtist(artist *model.Artist) error { - _, err := repo.db.Exec( +func UpdateArtist(db *sqlx.DB, artist *model.Artist) error { + _, err := db.Exec( "UPDATE artist "+ "SET name=$2, website=$3, avatar=$4 "+ "WHERE id=$1", @@ -128,27 +131,22 @@ func (repo *MusicRepositoryPostgres) UpdateArtist(artist *model.Artist) error { artist.Website, artist.Avatar, ) - 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 + 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 +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 } diff --git a/controller/release.go b/controller/release.go new file mode 100644 index 0000000..a22d157 --- /dev/null +++ b/controller/release.go @@ -0,0 +1,327 @@ +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/session.go b/controller/session.go index 08b6bdb..875d50f 100644 --- a/controller/session.go +++ b/controller/session.go @@ -38,10 +38,10 @@ func GetSessionFromRequest(app *app.AppState, r *http.Request) (*model.Session, account, _ := app.AccountService.GetByID(session.Account.ID) msg += " (Account \"" + account.Username + "\")" } - app.LogService.Warn(model.LOG_ACCOUNT, msg) + app.Log.Warn(model.LOG_ACCOUNT, msg) err = DeleteSession(app.DB, session.Token) if err != nil { - app.LogService.Warn(model.LOG_ACCOUNT, "Failed to delete affected session") + app.Log.Warn(model.LOG_ACCOUNT, "Failed to delete affected session") } return nil, nil } diff --git a/controller/track.go b/controller/track.go new file mode 100644 index 0000000..27f4afc --- /dev/null +++ b/controller/track.go @@ -0,0 +1,181 @@ +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 deleted file mode 100644 index 01b0fc2..0000000 --- a/errors/notexist.go +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index fe61771..0000000 --- a/errors/notexist_test.go +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 7309694..0000000 --- a/errors/validation.go +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 8a6783c..0000000 --- a/errors/validation_test.go +++ /dev/null @@ -1,26 +0,0 @@ -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 7086265..b662cc8 100644 --- a/main.go +++ b/main.go @@ -24,16 +24,12 @@ 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" - musicService "arimelody-web/service/music" + "arimelody-web/view" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -51,7 +47,6 @@ 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") @@ -97,7 +92,7 @@ func main() { app.DB = psqlDB logRepo := logRepo.NewLogRepositoryPostgres(psqlDB) - app.LogService = logService.NewLogService( + app.Log = logService.NewLogService( logRepo, log.New(os.Stderr, "logger", model.DEFAULT_LOG_FLAGS), ) @@ -108,12 +103,6 @@ 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] @@ -150,7 +139,7 @@ func main() { logger.Fatalf("FATAL: Failed to create TOTP method: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "TOTP method \"%s\" for \"%s\" created via config utility.", totp.Name, account.Username) + app.Log.Info(model.LOG_ACCOUNT, "TOTP method \"%s\" for \"%s\" created via config utility.", totp.Name, account.Username) url := controller.GenerateTOTPURI(account.Username, totp.Secret) logger.Printf("%s\n", url) return @@ -176,7 +165,7 @@ func main() { logger.Fatalf("FATAL: Failed to create TOTP method: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "TOTP method \"%s\" for \"%s\" deleted via config utility.", totpName, account.Username) + app.Log.Info(model.LOG_ACCOUNT, "TOTP method \"%s\" for \"%s\" deleted via config utility.", totpName, account.Username) logger.Printf("TOTP method \"%s\" deleted.\n", totpName) return @@ -242,7 +231,7 @@ func main() { if err != nil { logger.Fatalf("FATAL: Failed to clean up TOTP methods: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "TOTP methods pruned via config utility.") + app.Log.Info(model.LOG_ACCOUNT, "TOTP methods pruned via config utility.") logger.Printf("Cleaned up dangling TOTP methods successfully.\n") return @@ -253,7 +242,7 @@ func main() { logger.Fatalf("FATAL: Failed to create invite code: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "Invite generted via config utility (%s).", invite.Code) + app.Log.Info(model.LOG_ACCOUNT, "Invite generted via config utility (%s).", invite.Code) logger.Printf( "Here you go! This code expires in %d hours: %s\n", int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())), @@ -268,7 +257,7 @@ func main() { logger.Fatalf("FATAL: Failed to delete invites: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "Invites purged via config utility.") + app.Log.Info(model.LOG_ACCOUNT, "Invites purged via config utility.") logger.Printf("Invites deleted successfully.\n") return @@ -321,7 +310,7 @@ func main() { logger.Fatalf("FATAL: Failed to update password: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "Password for '%s' updated via config utility.", account.Username) + app.Log.Info(model.LOG_ACCOUNT, "Password for '%s' updated via config utility.", account.Username) logger.Printf("Password for \"%s\" updated successfully.\n", account.Username) return @@ -353,7 +342,7 @@ func main() { logger.Fatalf("FATAL: Failed to delete account: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "Account '%s' deleted via config utility.", account.Username) + app.Log.Info(model.LOG_ACCOUNT, "Account '%s' deleted via config utility.", account.Username) logger.Printf("Account \"%s\" deleted successfully.\n", account.Username) return @@ -378,7 +367,7 @@ func main() { logger.Fatalf("FATAL: Failed to lock account: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "Account '%s' locked via config utility.", account.Username) + app.Log.Info(model.LOG_ACCOUNT, "Account '%s' locked via config utility.", account.Username) logger.Printf("Account \"%s\" locked successfully.\n", account.Username) return @@ -403,13 +392,13 @@ func main() { logger.Fatalf("FATAL: Failed to unlock account: %v\n", err) } - app.LogService.Info(model.LOG_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username) + app.Log.Info(model.LOG_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username) logger.Printf("Account \"%s\" unlocked successfully.\n", account.Username) return case "logs": // TODO: add log search parameters - logs, err := app.LogService.Search([]model.LogLevel{}, []string{}, "", 100, 0) + logs, err := app.Log.Search([]model.LogLevel{}, []string{}, "", 100, 0) if err != nil { logger.Fatalf("FATAL: Failed to fetch logs: %v\n", err) } diff --git a/model/account.go b/model/account.go index 80e0a92..67424b7 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 80a84d4..bc8d69b 100644 --- a/model/app/appstate.go +++ b/model/app/appstate.go @@ -6,12 +6,8 @@ import ( "github.com/jmoiron/sqlx" "arimelody-web/model/twitch" - - logService "arimelody-web/service/log" - //inviteService "arimelody-web/service/invite" accountService "arimelody-web/service/account" - //sessionService "arimelody-web/service/session" - musicService "arimelody-web/service/music" + "arimelody-web/service/log" ) type ( @@ -49,13 +45,10 @@ type ( AppState struct { DB *sqlx.DB Config Config + Log *log.LogService Twitch *twitch.State PublicFS embed.FS - LogService *logService.LogService - //InviteService *inviteService.InviteService AccountService *accountService.AccountService - //SesisonService *sessionService.SessionService - MusicService *musicService.MusicService } ) diff --git a/model/artist.go b/model/artist.go index b9f940b..746a7dd 100644 --- a/model/artist.go +++ b/model/artist.go @@ -9,11 +9,9 @@ type ( } ) -const DEFAULT_AVATAR_URL = "/img/default-avatar.png" - func (artist Artist) GetAvatar() string { if artist.Avatar == "" { - return DEFAULT_AVATAR_URL + return "/img/default-avatar.png" } return artist.Avatar } diff --git a/model/artist_test.go b/model/artist_test.go index 8e39479..feb9a18 100644 --- a/model/artist_test.go +++ b/model/artist_test.go @@ -1,22 +1,21 @@ -package model_test +package model import ( - "arimelody-web/model" - "testing" + "testing" ) func Test_Artist_GetAvatar(t *testing.T) { want := "testavatar.png" - artist := model.Artist{ Avatar: want } + artist := 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 = model.Artist{} + artist = Artist{} - want = model.DEFAULT_AVATAR_URL + want = "/img/default-avatar.png" 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 f5f8feb..2080d83 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 2458dac..b368094 100644 --- a/model/link_test.go +++ b/model/link_test.go @@ -1,12 +1,11 @@ -package model_test +package model import ( - "arimelody-web/model" - "testing" + "testing" ) func Test_Link_NormaliseName(t *testing.T) { - link := model.Link{ + link := 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 3f00d4d..46f4460 100644 --- a/model/release.go +++ b/model/release.go @@ -34,8 +34,6 @@ const ( EP ReleaseType = "EP" Compilation ReleaseType = "compilation" Upcoming ReleaseType = "upcoming" - - DEFAULT_RELEASE_ARTWORK_URL = "/img/default-cover-art.png" ) // GETTERS @@ -54,7 +52,7 @@ func (release Release) PrintReleaseDate() string { func (release Release) GetArtwork() string { if release.Artwork == "" { - return DEFAULT_RELEASE_ARTWORK_URL + return "/img/default-cover-art.png" } return release.Artwork } @@ -95,20 +93,3 @@ 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 37ef775..fc0e221 100644 --- a/model/release_test.go +++ b/model/release_test.go @@ -1,7 +1,6 @@ -package model_test +package model import ( - "arimelody-web/model" "strings" "testing" "time" @@ -11,7 +10,7 @@ import ( func Test_Release(t *testing.T) { t.Run("prints correct description HTML", func(t *testing.T) { - release := model.Release{ + release := Release{ Description: "this is\na test\ndescription!", } @@ -25,7 +24,7 @@ func Test_Release(t *testing.T) { }) t.Run("prints correct release date", func(t *testing.T) { - release := model.Release{ + release := Release{ ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC), } @@ -35,37 +34,37 @@ func Test_Release(t *testing.T) { t.Run("returns correct artwork", func(t *testing.T) { artwork := "testartwork.png" - release := model.Release{ Artwork: artwork } + release := Release{ Artwork: artwork } assert.Equal(t, release.GetArtwork(), artwork) }) t.Run("returns placeholder artwork when empty", func(t *testing.T) { - release := model.Release{} - assert.Equal(t, release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL) + release := Release{} + assert.Equal(t, release.GetArtwork(), "/img/default-cover-art.png") }) t.Run("singles", func(t *testing.T) { - release := model.Release{ - Tracks: []*model.Track{}, + release := Release{ + Tracks: []*Track{}, } t.Run("false when no tracks are present", func(t *testing.T) { assert.Equal(t, release.IsSingle(), false) }) - release.Tracks = append(release.Tracks, &model.Track{}) + release.Tracks = append(release.Tracks, &Track{}) t.Run("true when one track is present", func(t *testing.T) { assert.Equal(t, release.IsSingle(), true) }) - release.Tracks = append(release.Tracks, &model.Track{}) + release.Tracks = append(release.Tracks, &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 := model.Release { + release := Release { ReleaseDate: time.Now(), } @@ -85,23 +84,23 @@ func Test_Release(t *testing.T) { artist3 := "idk" artist4 := "guest" - release := model.Release{} + release := 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, - &model.Credit{ Artist: &model.Artist{ Name: artist1 }, Primary: true }, + &Credit{ Artist: 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, []*model.Credit{ - { Artist: &model.Artist{ Name: artist2 }, Primary: true }, - { Artist: &model.Artist{ Name: artist3 }, Primary: false }, - { Artist: &model.Artist{ Name: artist4 }, Primary: true }, + release.Credits = append(release.Credits, []*Credit{ + { Artist: Artist{ Name: artist2 }, Primary: true }, + { Artist: Artist{ Name: artist3 }, Primary: false }, + { Artist: Artist{ Name: artist4 }, Primary: true }, }...) t.Run("can get only unique primary artist names", func(t *testing.T) { assert.Equal( @@ -147,37 +146,4 @@ 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 091deff..eb64b10 100644 --- a/model/track_test.go +++ b/model/track_test.go @@ -1,12 +1,11 @@ -package model_test +package model import ( - "arimelody-web/model" - "testing" + "testing" ) func Test_Track_DescriptionHTML(t *testing.T) { - track := model.Track{ + track := Track{ Description: "this is\na test\ndescription!", } @@ -20,7 +19,7 @@ func Test_Track_DescriptionHTML(t *testing.T) { } func Test_Track_LyricsHTML(t *testing.T) { - track := model.Track{ + track := Track{ Lyrics: "these are\ntest\nlyrics!", } diff --git a/repository/account/interface.go b/repository/account/interface.go index ba90073..fda2b1a 100644 --- a/repository/account/interface.go +++ b/repository/account/interface.go @@ -5,8 +5,6 @@ import "arimelody-web/model" type AccountRepository interface { GetAll() ([]*model.Account, error) GetCount() (int, error) - // Fetches an account by ID, returning an error if one was encountered. - // If the account does not exist, both response fields are nil. GetByID(id string) (*model.Account, error) GetByUsername(username string) (*model.Account, error) GetByEmail(email string) (*model.Account, error) @@ -18,20 +16,21 @@ type AccountRepository interface { // Create an account, returning the new account ID. Create(username string, password string, email *string, avatarURL *string) (string, error) - // Deprecated in favour of more specialised Update* and Remove* functions. + // Deprecated in favour of more specialised Change* and Remove* functions. Update(id string, username string, password string, email *string, avatarUrl *string) error - UpdateUsername(id string, username string) error - UpdatePassword(id string, password string) error - UpdateEmail(id string, email string) error + ChangeUsername(id string, username string) error + ChangePassword(id string, password string) error + ChangeEmail(id string, email string) error RemoveEmail(id string) error - UpdateAvatarURL(id string, avatarURL string) error + ChangeAvatarURL(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) + Lock(id string) error + Unlock(id string) error } diff --git a/repository/account/memory.go b/repository/account/memory.go index 7f886d1..1b94167 100644 --- a/repository/account/memory.go +++ b/repository/account/memory.go @@ -1,21 +1,22 @@ package account import ( - "arimelody-web/errors" "arimelody-web/model" "database/sql" - "slices" + "errors" "strconv" ) -type AccountRepositoryMemory struct { - accounts []*model.Account -} +type ( + AccountRepositoryMemory struct { + accounts []*model.Account + } +) var _ AccountRepository = new(AccountRepositoryMemory) -func NewAccountRepositoryMemory(accounts []*model.Account) *AccountRepositoryMemory { - return &AccountRepositoryMemory{ accounts: accounts } +func NewAccountRepositoryMemory() *AccountRepositoryMemory { + return &AccountRepositoryMemory{ accounts: make([]*model.Account, 0) } } func (repo *AccountRepositoryMemory) GetAll() ([]*model.Account, error) { @@ -25,11 +26,10 @@ func (repo *AccountRepositoryMemory) GetCount() (int, error) { return len(repo.accounts), nil } func (repo *AccountRepositoryMemory) GetByID(id string) (*model.Account, error) { - 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 + for _, account := range repo.accounts { + if account.ID == id { return account, nil } + } + return nil, nil } func (repo *AccountRepositoryMemory) GetByUsername(username string) (*model.Account, error) { for _, account := range repo.accounts { @@ -49,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.NewNotExistError("Failed to fetch other acccounts by username") + return "", errors.New("Failed to fetch other acccounts by username") } else if account != nil { - return "", errors.NewNotExistError("Account with this username already exists") + return "", errors.New("Account with this username already exists") } emailRef := "" @@ -73,12 +73,12 @@ func (repo *AccountRepositoryMemory) Create(username string, password string, em } // Intended for large profile updates. For smaller adjusments, -// more specialised Update* and Remove* functions should be used. +// more specialised Change* 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.NewNotExistError("Failed to fetch other acccounts by username") + return errors.New("Failed to fetch other acccounts by username") } else if account != nil && account.ID != id { - return errors.NewNotExistError("Account with this username already exists") + return errors.New("Account with this username already exists") } account, err := repo.GetByID(id) @@ -93,32 +93,32 @@ func (repo *AccountRepositoryMemory) Update(id string, username string, password return nil } -func (repo *AccountRepositoryMemory) UpdateUsername(id string, username string) error { +func (repo *AccountRepositoryMemory) ChangeUsername(id string, username string) error { if account, err := repo.GetByUsername(username); err != nil { - return errors.NewNotExistError("Failed to fetch other acccounts by username") + return errors.New("Failed to fetch other acccounts by username") } else if account != nil && account.ID != id { - return errors.NewNotExistError("Account with this username already exists") + return errors.New("Account with this username already exists") } account, err := repo.GetByID(id) if err != nil { return err } - if account == nil { return errors.NewNotExistError("Account does not exist") } + if account == nil { return errors.New("Account does not exist") } account.Username = username return nil } -func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string) error { +func (repo *AccountRepositoryMemory) ChangePassword(id string, password string) error { account, err := repo.GetByID(id) if err != nil { return err } - if account == nil { return errors.NewNotExistError("Account does not exist") } + if account == nil { return errors.New("Account does not exist") } account.Password = password return nil } -func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error { +func (repo *AccountRepositoryMemory) ChangeEmail(id string, email string) error { account, err := repo.GetByID(id) if err != nil { return err } - if account == nil { return errors.NewNotExistError("Account does not exist") } + if account == nil { return errors.New("Account does not exist") } account.Email.Valid = true account.Email.String = email @@ -127,16 +127,16 @@ 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.NewNotExistError("Account does not exist") } + if account == nil { return errors.New("Account does not exist") } account.Email.Valid = false account.Email.String = "" return nil } -func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string) error { +func (repo *AccountRepositoryMemory) ChangeAvatarURL(id string, avatarURL string) error { account, err := repo.GetByID(id) if err != nil { return err } - if account == nil { return errors.NewNotExistError("Account does not exist") } + if account == nil { return errors.New("Account does not exist") } account.AvatarURL.Valid = true account.AvatarURL.String = avatarURL @@ -145,13 +145,31 @@ 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.NewNotExistError("Account does not exist") } + if account == nil { return errors.New("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) { @@ -166,23 +184,15 @@ func (repo *AccountRepositoryMemory) ResetFails(id string) error { account.FailAttempts = 0 return nil } -func (repo *AccountRepositoryMemory) SetLocked(id string, locked bool) error { +func (repo *AccountRepositoryMemory) Lock(id string) error { account, err := repo.GetByID(id) if err != nil { return err } - account.Locked = locked + account.Locked = true 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 +func (repo *AccountRepositoryMemory) Unlock(id string) error { + account, err := repo.GetByID(id) + if err != nil { return err } + account.Locked = false + return nil } diff --git a/repository/account/postgres.go b/repository/account/postgres.go index 03d797f..74ac4c3 100644 --- a/repository/account/postgres.go +++ b/repository/account/postgres.go @@ -8,9 +8,11 @@ import ( _ "github.com/lib/pq" ) -type AccountRepositoryPostgres struct { - db *sqlx.DB -} +type ( + AccountRepositoryPostgres struct { + db *sqlx.DB + } +) var _ AccountRepository = new(AccountRepositoryPostgres) @@ -120,21 +122,21 @@ func (repo *AccountRepositoryPostgres) Update( return err } -func (repo *AccountRepositoryPostgres) UpdateUsername(id string, username string) error { +func (repo *AccountRepositoryPostgres) ChangeUsername(id string, username string) error { _, err := repo.db.Exec( "UPDATE account SET username=$2 WHERE id=$1", id, username, ) return err } -func (repo *AccountRepositoryPostgres) UpdatePassword(id string, password string) error { +func (repo *AccountRepositoryPostgres) ChangePassword(id string, password string) error { _, err := repo.db.Exec( "UPDATE account SET password=$2 WHERE id=$1", id, password, ) return err } -func (repo *AccountRepositoryPostgres) UpdateEmail(id string, email string) error { +func (repo *AccountRepositoryPostgres) ChangeEmail(id string, email string) error { _, err := repo.db.Exec( "UPDATE account SET email=$2 WHERE id=$1", id, email, @@ -145,7 +147,7 @@ func (repo *AccountRepositoryPostgres) RemoveEmail(id string) error { _, err := repo.db.Exec("UPDATE account SET email=NULL WHERE id=$1", id) return err } -func (repo *AccountRepositoryPostgres) UpdateAvatarURL(id string, avatarURL string) error { +func (repo *AccountRepositoryPostgres) ChangeAvatarURL(id string, avatarURL string) error { _, err := repo.db.Exec( "UPDATE account SET avatar_url=$2 WHERE id=$1", id, avatarURL, @@ -157,6 +159,11 @@ 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) { @@ -170,13 +177,12 @@ func (repo *AccountRepositoryPostgres) ResetFails(id string) error { return err } -func (repo *AccountRepositoryPostgres) SetLocked(id string, locked bool) error { - _, err := repo.db.Exec("UPDATE account SET locked = $2 WHERE id=$1", id, locked) +func (repo *AccountRepositoryPostgres) Lock(id string) error { + _, err := repo.db.Exec("UPDATE account SET locked = true WHERE id=$1", id) 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 +func (repo *AccountRepositoryPostgres) Unlock(id string) error { + _, err := repo.db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", id) + return err } diff --git a/repository/log/postgres.go b/repository/log/postgres.go index b813017..ba1246a 100644 --- a/repository/log/postgres.go +++ b/repository/log/postgres.go @@ -8,9 +8,11 @@ import ( _ "github.com/lib/pq" ) -type LogRepositoryPostgres struct { - db *sqlx.DB -} +type ( + LogRepositoryPostgres struct { + db *sqlx.DB + } +) var _ LogRepository = new(LogRepositoryPostgres) diff --git a/repository/music/artist_memory.go b/repository/music/artist_memory.go deleted file mode 100644 index ff61eee..0000000 --- a/repository/music/artist_memory.go +++ /dev/null @@ -1,135 +0,0 @@ -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/repository/music/interface.go b/repository/music/interface.go deleted file mode 100644 index 5fdc3ac..0000000 --- a/repository/music/interface.go +++ /dev/null @@ -1,110 +0,0 @@ -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 deleted file mode 100644 index 3e99612..0000000 --- a/repository/music/release_memory.go +++ /dev/null @@ -1,228 +0,0 @@ -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 deleted file mode 100644 index e98a165..0000000 --- a/repository/music/release_postgres.go +++ /dev/null @@ -1,303 +0,0 @@ -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 deleted file mode 100644 index 7ad574b..0000000 --- a/repository/music/track_memory.go +++ /dev/null @@ -1,123 +0,0 @@ -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 deleted file mode 100644 index b0b0946..0000000 --- a/repository/music/track_postgres.go +++ /dev/null @@ -1,137 +0,0 @@ -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 bf7afd4..730fc8b 100644 --- a/service/account/account.go +++ b/service/account/account.go @@ -1,10 +1,9 @@ package account import ( - "arimelody-web/errors" "arimelody-web/model" repository "arimelody-web/repository/account" - "arimelody-web/service/validator" + "errors" "log" ) @@ -29,24 +28,15 @@ func (s *AccountService) GetCount() (int, error) { } func (s *AccountService) GetByID(id string) (*model.Account, error) { - 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 + return s.repo.GetByID(id) } func (s *AccountService) GetByUsername(username string) (*model.Account, error) { - 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 + return s.repo.GetByUsername(username) } func (s *AccountService) GetByEmail(email string) (*model.Account, error) { - 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 + return s.repo.GetByEmail(email) } func (s *AccountService) Create( @@ -55,13 +45,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 } @@ -71,60 +61,58 @@ func (s *AccountService) Create( } func (s *AccountService) ChangeUsername(id string, username 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 err := s.repo.UpdateUsername(id, username); err != nil { return err } + if len(username) == 0 { return errors.New("Username cannot be empty") } + if err := s.repo.ChangeUsername(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.NewValidationError("Password cannot be empty") } - if err := s.repo.UpdatePassword(id, password); err != nil { return err } + if len(password) == 0 { return errors.New("Password cannot be empty") } + if err := s.repo.ChangePassword(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.UpdateEmail(id, email); err != nil { return err } + if err := s.repo.ChangeEmail(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.ChangeAvatarURL(id, avatarURL); err != nil { + return err + } + s.log.Printf("Changed avatar URL for %s to '%s'", id, avatarURL) return nil } -func (s *AccountService) IncrementFails(id string) (int, error) { - num, err := s.repo.IncrementFails(id) - if err != nil { return 0, err } - s.log.Printf("Incremented auth failures for account %s (now %d)", id, num) - return num, nil +func (s *AccountService) Delete(accountID string) error { + return s.repo.Delete(accountID) } -func (s *AccountService) ResetFails(id string) (error) { - if err := s.repo.ResetFails(id); err != nil { return err } - s.log.Printf("Reset auth failures for account %s", id) - return nil +func (s *AccountService) IncrementFails(accountID string) (int, error) { + return s.repo.IncrementFails(accountID) } -func (s *AccountService) Lock(id string) error { - if err := s.repo.SetLocked(id, true); err != nil { return err } - s.log.Printf("Locked account %s", id) - return nil +func (s *AccountService) ResetFails(accountID string) (error) { + return s.repo.ResetFails(accountID) } -func (s *AccountService) Unlock(id string) error { - if err := s.repo.SetLocked(id, false); err != nil { return err } - s.log.Printf("Unlocked account %s", id) - return nil +func (s *AccountService) Lock(accountID string) error { + return s.repo.Lock(accountID) } -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 +func (s *AccountService) Unlock(accountID string) error { + return s.repo.Unlock(accountID) } diff --git a/service/account/account_test.go b/service/account/account_test.go index 5eec680..6487d64 100644 --- a/service/account/account_test.go +++ b/service/account/account_test.go @@ -1,19 +1,16 @@ -package account_test +package account import ( "arimelody-web/model" - repository "arimelody-web/repository/account" - service "arimelody-web/service/account" - "arimelody-web/errors" + accountRepo "arimelody-web/repository/account" "log" "os" "testing" - - "gotest.tools/v3/assert" + "gotest.tools/v3/assert" ) var ( - s *service.AccountService + service *AccountService ) func init() { @@ -21,8 +18,8 @@ func init() { if err != nil { panic(err) } defer devNullFile.Close() - repo := repository.NewAccountRepositoryMemory(make([]*model.Account, 0)) - s = service.NewAccountService( + repo := accountRepo.NewAccountRepositoryMemory() + service = NewAccountService( repo, log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS), ) @@ -44,14 +41,14 @@ func Test_Account(t *testing.T) { username := "testificate" password := "the amazing digital data breach" email := "goober@arimelody.space" - avatarURL := "/img/account-avatar.webp" + avatarURL := "/img/default-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 := s.GetCount(); err != nil { + if num, err := service.GetCount(); err != nil { t.Errorf("Failed to get number of accounts: %v", err) } else { assert.Equal(t, num, 0) @@ -59,8 +56,8 @@ func Test_Account(t *testing.T) { }) t.Run("service returns empty array", func(t *testing.T) { - if accounts, err := s.GetAll(); err != nil { - t.Errorf("Failed to get accounts: %v", err) + if accounts, err := service.GetAll(); err != nil { + t.Errorf("Failed to get number of accounts: %v", err) } else { assert.Equal(t, len(accounts), 0) } @@ -68,28 +65,13 @@ func Test_Account(t *testing.T) { }) t.Run("can create account", func(t *testing.T) { - id, err = s.Create(username, password, &email, &avatarURL) + id, err = service.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 := s.GetByID(id) + account, err := service.GetByID(id) if err != nil { t.Errorf("Failed to get account after creation: %v", err) } @@ -102,7 +84,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 := s.GetByUsername(username) + account, err := service.GetByUsername(username) if err != nil { t.Errorf("Failed to get account after creation: %v", err) } @@ -115,7 +97,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 := s.GetByEmail(email) + account, err := service.GetByEmail(email) if err != nil { t.Errorf("Failed to get account after creation: %v", err) } @@ -131,7 +113,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 := s.GetCount(); err != nil { + if num, err := service.GetCount(); err != nil { t.Errorf("Failed to get number of accounts: %v", err) } else { assert.Equal(t, num, 1) @@ -139,8 +121,8 @@ func Test_Account(t *testing.T) { }) t.Run("service returns array with one account", func(t *testing.T) { - if accounts, err := s.GetAll(); err != nil { - t.Errorf("Failed to get accounts: %v", err) + if accounts, err := service.GetAll(); err != nil { + t.Errorf("Failed to get number of accounts: %v", err) } else { assert.Equal(t, len(accounts), 1) } @@ -148,7 +130,7 @@ func Test_Account(t *testing.T) { }) t.Run("can't create duplicate account", func(t *testing.T) { - _, err := s.Create(username, password, &email, &avatarURL) + _, err := service.Create(username, password, &email, &avatarURL) if err == nil { t.Error("Duplicate account was created") } @@ -156,11 +138,11 @@ func Test_Account(t *testing.T) { t.Run("can change username", func(t *testing.T) { testUsername := "some_other_name" - if err := s.ChangeUsername(id, testUsername); err != nil { + if err := service.ChangeUsername(id, testUsername); err != nil { t.Errorf("Failed to change username: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -169,21 +151,19 @@ func Test_Account(t *testing.T) { } t.Run("but not to an invalid value", func(t *testing.T) { - if err := s.ChangeUsername(id, ""); err == nil { + if err := service.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 := s.ChangePassword(id, testPassword); err != nil { + if err := service.ChangePassword(id, testPassword); err != nil { t.Errorf("Failed to change password: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -192,21 +172,19 @@ func Test_Account(t *testing.T) { } t.Run("but not to an invalid value", func(t *testing.T) { - if err := s.ChangePassword(id, ""); err == nil { + if err := service.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 := s.ChangeEmail(id, testEmail); err != nil { + if err := service.ChangeEmail(id, testEmail); err != nil { t.Errorf("Failed to change email: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -216,11 +194,11 @@ func Test_Account(t *testing.T) { }) t.Run("can remove email", func(t *testing.T) { - if err := s.ChangeEmail(id, ""); err != nil { + if err := service.ChangeEmail(id, ""); err != nil { t.Errorf("Failed to change email: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -231,11 +209,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 := s.ChangeAvatarURL(id, testAvatarURL); err != nil { + if err := service.ChangeAvatarURL(id, testAvatarURL); err != nil { t.Errorf("Failed to change avatar URL: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -245,11 +223,11 @@ func Test_Account(t *testing.T) { }) t.Run("can remove avatar URL", func(t *testing.T) { - if err := s.ChangeAvatarURL(id, ""); err != nil { + if err := service.ChangeAvatarURL(id, ""); err != nil { t.Errorf("Failed to change avatar URL: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -259,13 +237,13 @@ func Test_Account(t *testing.T) { }) t.Run("can increment auth failures", func(t *testing.T) { - if num, err := s.IncrementFails(id); err != nil { + if num, err := service.IncrementFails(id); err != nil { t.Errorf("Failed to increment account auth failures: %v", err) } else { assert.Equal(t, num, 1) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -275,11 +253,11 @@ func Test_Account(t *testing.T) { }) t.Run("can reset auth failures", func(t *testing.T) { - if err := s.ResetFails(id); err != nil { + if err := service.ResetFails(id); err != nil { t.Errorf("Failed to reset account auth failures: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -289,11 +267,11 @@ func Test_Account(t *testing.T) { }) t.Run("can lock account", func(t *testing.T) { - if err := s.Lock(id); err != nil { + if err := service.Lock(id); err != nil { t.Errorf("Failed to lock account: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -303,11 +281,11 @@ func Test_Account(t *testing.T) { }) t.Run("can unlock account", func(t *testing.T) { - if err := s.Unlock(id); err != nil { + if err := service.Unlock(id); err != nil { t.Errorf("Failed to unlock account: %v", err) } - if account, err := s.GetByID(id); err != nil { + if account, err := service.GetByID(id); err != nil { t.Errorf("Failed to get account: %v", err) } else if account == nil { t.Error("Account is nil after update") @@ -317,14 +295,12 @@ func Test_Account(t *testing.T) { }) t.Run("can delete account", func(t *testing.T) { - if err = s.Delete(id); err != nil { + if err = service.Delete(id); err != nil { t.Errorf("Failed to delete account: %v", err) } - if account, err := s.GetByID(id); err != nil { - if !errors.IsNotExistError(err) { - t.Errorf("Failed to get account after deletion: %v", err) - } + if account, err := service.GetByID(id); err != nil { + t.Errorf("Failed to get account after deletion: %v", err) } else if account != nil { t.Error("Account still exists after deletion") } @@ -332,76 +308,56 @@ 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 := s.Create("", password, &email, &avatarURL); err == nil { + if _, err := service.Create("", password, &email, &avatarURL); err == nil { t.Error("Could create account with empty username") } }) t.Run("password", func(t *testing.T) { - if _, err := s.Create(username, "", &email, &avatarURL); err == nil { + if _, err := service.Create(username, "", &email, &avatarURL); err == nil { t.Error("Could create account with empty password") } }) t.Run("email", func(t *testing.T) { testEmail := "" - if _, err := s.Create(username, password, &testEmail, &avatarURL); err == nil { + if _, err := service.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 := s.ChangeUsername(garbageAccountID, "some-username"); err == nil { + if err := service.ChangeUsername(garbageAccountID, "some-username"); err == nil { t.Error("Could update non-existent account's username") } }) t.Run("password", func(t *testing.T) { - if err := s.ChangePassword(garbageAccountID, "some-password"); err == nil { + if err := service.ChangePassword(garbageAccountID, "some-password"); err == nil { t.Error("Could update non-existent account's password") } }) t.Run("email", func(t *testing.T) { - if err := s.ChangeEmail(garbageAccountID, "some-email@real.gov"); err == nil { + if err := service.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 := s.ChangeAvatarURL(garbageAccountID, "/img/null.webp"); err == nil { + if err := service.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 bf06b7b..21f1650 100644 --- a/service/log/log.go +++ b/service/log/log.go @@ -3,7 +3,6 @@ package log import ( "arimelody-web/model" repository "arimelody-web/repository/log" - "arimelody-web/errors" "fmt" "log" "time" @@ -44,10 +43,7 @@ func (s *LogService) Warn(logType string, format string, args ...any) { } func (s *LogService) Fetch(id string) (*model.Log, error) { - 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 + return s.repo.Get(id) } func (s *LogService) Search( diff --git a/service/music/artist.go b/service/music/artist.go deleted file mode 100644 index aa643af..0000000 --- a/service/music/artist.go +++ /dev/null @@ -1,82 +0,0 @@ -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 deleted file mode 100644 index 1a59645..0000000 --- a/service/music/artist_test.go +++ /dev/null @@ -1,225 +0,0 @@ -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 deleted file mode 100644 index f3037cc..0000000 --- a/service/music/music.go +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 72447a4..0000000 --- a/service/music/music_test.go +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 494477a..0000000 --- a/service/music/release.go +++ /dev/null @@ -1,180 +0,0 @@ -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 deleted file mode 100644 index d14a697..0000000 --- a/service/music/release_test.go +++ /dev/null @@ -1,374 +0,0 @@ -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 deleted file mode 100644 index 5a9e993..0000000 --- a/service/music/track.go +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index cb23857..0000000 --- a/service/validator/validator.go +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 0de2353..0000000 --- a/service/validator/validator_test.go +++ /dev/null @@ -1,39 +0,0 @@ -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 ed48c1b..9cb4e59 100644 --- a/view/music.go +++ b/view/music.go @@ -22,7 +22,7 @@ func MusicHandler(app *app.AppState) http.Handler { return } - release, err := app.MusicService.GetFullReleaseByID(r.URL.Path[1:]) + release, err := controller.GetRelease(app.DB, r.URL.Path[1:], true) 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 := app.MusicService.GetAllFullReleases(true, 0) + releases, err := controller.GetAllReleases(app.DB, true, 0, true) if err != nil { fmt.Printf("WARN: Failed to pull releases for catalog: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)