feat: v0.0.8

This commit is contained in:
Gabe Farrell 2025-06-16 21:55:39 -04:00
parent 00e7782be2
commit 80b6f4deaa
66 changed files with 1559 additions and 916 deletions

View file

@ -1,7 +1,6 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
@ -40,44 +39,43 @@ func GetAliasesHandler(store db.DB) http.HandlerFunc {
if artistIDStr != "" {
artistID, err := strconv.Atoi(artistIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("GetAliasesHandler: %w", err)).Msg("Invalid artist id")
l.Debug().AnErr("error", err).Msg("GetAliasesHandler: Invalid artist id")
utils.WriteError(w, "invalid artist_id", http.StatusBadRequest)
return
}
aliases, err = store.GetAllArtistAliases(ctx, int32(artistID))
if err != nil {
l.Err(fmt.Errorf("GetAliasesHandler: %w", err)).Msg("Failed to get artist aliases")
l.Err(err).Msg("GetAliasesHandler: Failed to get artist aliases")
utils.WriteError(w, "failed to retrieve aliases", http.StatusInternalServerError)
return
}
} else if albumIDStr != "" {
albumID, err := strconv.Atoi(albumIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("GetAliasesHandler: %w", err)).Msg("Invalid album id")
l.Debug().AnErr("error", err).Msg("GetAliasesHandler: Invalid album id")
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
return
}
aliases, err = store.GetAllAlbumAliases(ctx, int32(albumID))
if err != nil {
l.Err(fmt.Errorf("GetAliasesHandler: %w", err)).Msg("Failed to get album aliases")
l.Err(err).Msg("GetAliasesHandler: Failed to get album aliases")
utils.WriteError(w, "failed to retrieve aliases", http.StatusInternalServerError)
return
}
} else if trackIDStr != "" {
trackID, err := strconv.Atoi(trackIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("GetAliasesHandler: %w", err)).Msg("Invalid track id")
l.Debug().AnErr("error", err).Msg("GetAliasesHandler: Invalid track id")
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
return
}
aliases, err = store.GetAllTrackAliases(ctx, int32(trackID))
if err != nil {
l.Err(fmt.Errorf("GetAliasesHandler: %w", err)).Msg("Failed to get track aliases")
l.Err(err).Msg("GetAliasesHandler: Failed to get track aliases")
utils.WriteError(w, "failed to retrieve aliases", http.StatusInternalServerError)
return
}
}
utils.WriteJSON(w, http.StatusOK, aliases)
}
}
@ -88,7 +86,7 @@ func DeleteAliasHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msgf("DeleteAliasHandler: Got request with params: '%s'", r.URL.Query().Encode())
l.Debug().Msg("DeleteAliasHandler: Got request")
// Parse query parameters
artistIDStr := r.URL.Query().Get("artist_id")
@ -97,52 +95,56 @@ func DeleteAliasHandler(store db.DB) http.HandlerFunc {
alias := r.URL.Query().Get("alias")
if alias == "" || (artistIDStr == "" && albumIDStr == "" && trackIDStr == "") {
l.Debug().Msgf("DeleteAliasHandler: Request is missing required parameters")
l.Debug().Msg("DeleteAliasHandler: Request is missing required parameters")
utils.WriteError(w, "alias and artist_id, album_id, or track_id must be provided", http.StatusBadRequest)
return
}
if utils.MoreThanOneString(artistIDStr, albumIDStr, trackIDStr) {
l.Debug().Msgf("DeleteAliasHandler: Request is has more than one of artist_id, album_id, and track_id")
l.Debug().Msg("DeleteAliasHandler: Request has more than one of artist_id, album_id, and track_id")
utils.WriteError(w, "only one of artist_id, album_id, or track_id can be provided at a time", http.StatusBadRequest)
return
}
var err error
if artistIDStr != "" {
artistID, err := strconv.Atoi(artistIDStr)
var artistID int
artistID, err = strconv.Atoi(artistIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("DeleteAliasHandler: %w", err)).Msg("Invalid artist id")
l.Debug().AnErr("error", err).Msg("DeleteAliasHandler: Invalid artist id")
utils.WriteError(w, "invalid artist_id", http.StatusBadRequest)
return
}
err = store.DeleteArtistAlias(ctx, int32(artistID), alias)
if err != nil {
l.Err(fmt.Errorf("DeleteAliasHandler: %w", err)).Msg("Failed to delete artist alias")
l.Error().Err(err).Msg("DeleteAliasHandler: Failed to delete artist alias")
utils.WriteError(w, "failed to delete alias", http.StatusInternalServerError)
return
}
} else if albumIDStr != "" {
albumID, err := strconv.Atoi(albumIDStr)
var albumID int
albumID, err = strconv.Atoi(albumIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("DeleteAliasHandler: %w", err)).Msg("Invalid album id")
l.Debug().AnErr("error", err).Msg("DeleteAliasHandler: Invalid album id")
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
return
}
err = store.DeleteAlbumAlias(ctx, int32(albumID), alias)
if err != nil {
l.Err(fmt.Errorf("DeleteAliasHandler: %w", err)).Msg("Failed to delete album alias")
l.Error().Err(err).Msg("DeleteAliasHandler: Failed to delete album alias")
utils.WriteError(w, "failed to delete alias", http.StatusInternalServerError)
return
}
} else if trackIDStr != "" {
trackID, err := strconv.Atoi(trackIDStr)
var trackID int
trackID, err = strconv.Atoi(trackIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("DeleteAliasHandler: %w", err)).Msg("Invalid track id")
l.Debug().AnErr("error", err).Msg("DeleteAliasHandler: Invalid track id")
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
return
}
err = store.DeleteTrackAlias(ctx, int32(trackID), alias)
if err != nil {
l.Err(fmt.Errorf("DeleteAliasHandler: %w", err)).Msg("Failed to delete track alias")
l.Error().Err(err).Msg("DeleteAliasHandler: Failed to delete track alias")
utils.WriteError(w, "failed to delete alias", http.StatusInternalServerError)
return
}
@ -158,16 +160,18 @@ func CreateAliasHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msgf("CreateAliasHandler: Got request with params: '%s'", r.URL.Query().Encode())
l.Debug().Msg("CreateAliasHandler: Got request")
err := r.ParseForm()
if err != nil {
l.Debug().AnErr("error", err).Msg("CreateAliasHandler: Failed to parse form")
utils.WriteError(w, "invalid request body", http.StatusBadRequest)
return
}
alias := r.FormValue("alias")
if alias == "" {
l.Debug().Msg("CreateAliasHandler: Alias parameter missing")
utils.WriteError(w, "alias must be provided", http.StatusBadRequest)
return
}
@ -176,53 +180,54 @@ func CreateAliasHandler(store db.DB) http.HandlerFunc {
albumIDStr := r.URL.Query().Get("album_id")
trackIDStr := r.URL.Query().Get("track_id")
if alias == "" || (artistIDStr == "" && albumIDStr == "" && trackIDStr == "") {
l.Debug().Msgf("CreateAliasHandler: Request is missing required parameters")
utils.WriteError(w, "alias and artist_id, album_id, or track_id must be provided", http.StatusBadRequest)
if artistIDStr == "" && albumIDStr == "" && trackIDStr == "" {
l.Debug().Msg("CreateAliasHandler: Missing ID parameter")
utils.WriteError(w, "artist_id, album_id, or track_id must be provided", http.StatusBadRequest)
return
}
if utils.MoreThanOneString(artistIDStr, albumIDStr, trackIDStr) {
l.Debug().Msgf("CreateAliasHandler: Request is has more than one of artist_id, album_id, and track_id")
utils.WriteError(w, "only one of artist_id, album_id, or track_id can be provided at a time", http.StatusBadRequest)
l.Debug().Msg("CreateAliasHandler: Multiple ID parameters provided")
utils.WriteError(w, "only one of artist_id, album_id, or track_id can be provided", http.StatusBadRequest)
return
}
var id int
if artistIDStr != "" {
artistID, err := strconv.Atoi(artistIDStr)
id, err = strconv.Atoi(artistIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("CreateAliasHandler: %w", err)).Msg("Invalid artist id")
l.Debug().AnErr("error", err).Msg("CreateAliasHandler: Invalid artist id")
utils.WriteError(w, "invalid artist_id", http.StatusBadRequest)
return
}
err = store.SaveArtistAliases(ctx, int32(artistID), []string{alias}, "Manual")
err = store.SaveArtistAliases(ctx, int32(id), []string{alias}, "Manual")
if err != nil {
l.Err(fmt.Errorf("CreateAliasHandler: %w", err)).Msg("Failed to save artist alias")
l.Error().Err(err).Msg("CreateAliasHandler: Failed to save artist alias")
utils.WriteError(w, "failed to save alias", http.StatusInternalServerError)
return
}
} else if albumIDStr != "" {
albumID, err := strconv.Atoi(albumIDStr)
id, err = strconv.Atoi(albumIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("CreateAliasHandler: %w", err)).Msg("Invalid album id")
l.Debug().AnErr("error", err).Msg("CreateAliasHandler: Invalid album id")
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
return
}
err = store.SaveAlbumAliases(ctx, int32(albumID), []string{alias}, "Manual")
err = store.SaveAlbumAliases(ctx, int32(id), []string{alias}, "Manual")
if err != nil {
l.Err(fmt.Errorf("CreateAliasHandler: %w", err)).Msg("Failed to save album alias")
l.Error().Err(err).Msg("CreateAliasHandler: Failed to save album alias")
utils.WriteError(w, "failed to save alias", http.StatusInternalServerError)
return
}
} else if trackIDStr != "" {
trackID, err := strconv.Atoi(trackIDStr)
id, err = strconv.Atoi(trackIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("CreateAliasHandler: %w", err)).Msg("Invalid track id")
l.Debug().AnErr("error", err).Msg("CreateAliasHandler: Invalid track id")
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
return
}
err = store.SaveTrackAliases(ctx, int32(trackID), []string{alias}, "Manual")
err = store.SaveTrackAliases(ctx, int32(id), []string{alias}, "Manual")
if err != nil {
l.Err(fmt.Errorf("CreateAliasHandler: %w", err)).Msg("Failed to save track alias")
l.Error().Err(err).Msg("CreateAliasHandler: Failed to save track alias")
utils.WriteError(w, "failed to save alias", http.StatusInternalServerError)
return
}
@ -238,7 +243,7 @@ func SetPrimaryAliasHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msgf("SetPrimaryAliasHandler: Got request with params: '%s'", r.URL.Query().Encode())
l.Debug().Msg("SetPrimaryAliasHandler: Got request")
// Parse query parameters
artistIDStr := r.URL.Query().Get("artist_id")
@ -246,53 +251,60 @@ func SetPrimaryAliasHandler(store db.DB) http.HandlerFunc {
trackIDStr := r.URL.Query().Get("track_id")
alias := r.URL.Query().Get("alias")
if alias == "" || (artistIDStr == "" && albumIDStr == "" && trackIDStr == "") {
l.Debug().Msgf("SetPrimaryAliasHandler: Request is missing required parameters")
utils.WriteError(w, "alias and artist_id, album_id, or track_id must be provided", http.StatusBadRequest)
if alias == "" {
l.Debug().Msg("SetPrimaryAliasHandler: Missing alias parameter")
utils.WriteError(w, "alias must be provided", http.StatusBadRequest)
return
}
if artistIDStr == "" && albumIDStr == "" && trackIDStr == "" {
l.Debug().Msg("SetPrimaryAliasHandler: Missing ID parameter")
utils.WriteError(w, "artist_id, album_id, or track_id must be provided", http.StatusBadRequest)
return
}
if utils.MoreThanOneString(artistIDStr, albumIDStr, trackIDStr) {
l.Debug().Msgf("SetPrimaryAliasHandler: Request is has more than one of artist_id, album_id, and track_id")
utils.WriteError(w, "only one of artist_id, album_id, or track_id can be provided at a time", http.StatusBadRequest)
l.Debug().Msg("SetPrimaryAliasHandler: Multiple ID parameters provided")
utils.WriteError(w, "only one of artist_id, album_id, or track_id can be provided", http.StatusBadRequest)
return
}
var id int
var err error
if artistIDStr != "" {
artistID, err := strconv.Atoi(artistIDStr)
id, err = strconv.Atoi(artistIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("SetPrimaryAliasHandler: %w", err)).Msg("Invalid artist id")
l.Debug().AnErr("error", err).Msg("SetPrimaryAliasHandler: Invalid artist id")
utils.WriteError(w, "invalid artist_id", http.StatusBadRequest)
return
}
err = store.SetPrimaryArtistAlias(ctx, int32(artistID), alias)
err = store.SetPrimaryArtistAlias(ctx, int32(id), alias)
if err != nil {
l.Err(fmt.Errorf("SetPrimaryAliasHandler: %w", err)).Msg("Failed to set artist primary alias")
l.Error().Err(err).Msg("SetPrimaryAliasHandler: Failed to set artist primary alias")
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
return
}
} else if albumIDStr != "" {
albumID, err := strconv.Atoi(albumIDStr)
id, err = strconv.Atoi(albumIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("SetPrimaryAliasHandler: %w", err)).Msg("Invalid album id")
l.Debug().AnErr("error", err).Msg("SetPrimaryAliasHandler: Invalid album id")
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
return
}
err = store.SetPrimaryAlbumAlias(ctx, int32(albumID), alias)
err = store.SetPrimaryAlbumAlias(ctx, int32(id), alias)
if err != nil {
l.Err(fmt.Errorf("SetPrimaryAliasHandler: %w", err)).Msg("Failed to set album primary alias")
l.Error().Err(err).Msg("SetPrimaryAliasHandler: Failed to set album primary alias")
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
return
}
} else if trackIDStr != "" {
trackID, err := strconv.Atoi(trackIDStr)
id, err = strconv.Atoi(trackIDStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("SetPrimaryAliasHandler: %w", err)).Msg("Invalid track id")
l.Debug().AnErr("error", err).Msg("SetPrimaryAliasHandler: Invalid track id")
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
return
}
err = store.SetPrimaryTrackAlias(ctx, int32(trackID), alias)
err = store.SetPrimaryTrackAlias(ctx, int32(id), alias)
if err != nil {
l.Err(fmt.Errorf("SetPrimaryAliasHandler: %w", err)).Msg("Failed to set track primary alias")
l.Error().Err(err).Msg("SetPrimaryAliasHandler: Failed to set track primary alias")
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
return
}

View file

@ -1,7 +1,6 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
@ -16,45 +15,47 @@ func GenerateApiKeyHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msgf("GenerateApiKeyHandler: Received request with params: '%s'", r.URL.Query().Encode())
l.Debug().Msg("GenerateApiKeyHandler: Received request")
user := middleware.GetUserFromContext(ctx)
if user == nil {
l.Debug().Msg("GenerateApiKeyHandler: Invalid user retrieved from context")
l.Debug().Msg("GenerateApiKeyHandler: Invalid user context")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
r.ParseForm()
if err := r.ParseForm(); err != nil {
l.Debug().AnErr("error", err).Msg("GenerateApiKeyHandler: Failed to parse form")
utils.WriteError(w, "invalid request", http.StatusBadRequest)
return
}
label := r.FormValue("label")
if label == "" {
l.Debug().Msg("GenerateApiKeyHandler: Request rejected due to missing label")
l.Debug().Msg("GenerateApiKeyHandler: Missing label parameter")
utils.WriteError(w, "label is required", http.StatusBadRequest)
return
}
apiKey, err := utils.GenerateRandomString(48)
if err != nil {
l.Err(fmt.Errorf("GenerateApiKeyHandler: %w", err)).Msg("Failed to generate API key")
l.Error().Err(err).Msg("GenerateApiKeyHandler: Failed to generate API key")
utils.WriteError(w, "failed to generate api key", http.StatusInternalServerError)
return
}
opts := db.SaveApiKeyOpts{
key, err := store.SaveApiKey(ctx, db.SaveApiKeyOpts{
UserID: user.ID,
Key: apiKey,
Label: label,
}
l.Debug().Msgf("GenerateApiKeyHandler: Saving API key with options: %+v", opts)
key, err := store.SaveApiKey(ctx, opts)
})
if err != nil {
l.Err(fmt.Errorf("GenerateApiKeyHandler: %w", err)).Msg("Failed to save API key")
l.Error().Err(err).Msg("GenerateApiKeyHandler: Failed to save API key")
utils.WriteError(w, "failed to save api key", http.StatusInternalServerError)
return
}
l.Debug().Msgf("GenerateApiKeyHandler: Successfully saved API key with ID: %d", key.ID)
l.Debug().Msgf("GenerateApiKeyHandler: Successfully generated API key ID %d", key.ID)
utils.WriteJSON(w, http.StatusCreated, key)
}
}
@ -64,39 +65,36 @@ func DeleteApiKeyHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msgf("DeleteApiKeyHandler: Received request with params: '%s'", r.URL.Query().Encode())
l.Debug().Msg("DeleteApiKeyHandler: Received request")
user := middleware.GetUserFromContext(ctx)
if user == nil {
l.Debug().Msg("DeleteApiKeyHandler: User could not be verified (context user is nil)")
l.Debug().Msg("DeleteApiKeyHandler: Invalid user context")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
idStr := r.URL.Query().Get("id")
if idStr == "" {
l.Debug().Msg("DeleteApiKeyHandler: Request rejected due to missing ID")
l.Debug().Msg("DeleteApiKeyHandler: Missing id parameter")
utils.WriteError(w, "id is required", http.StatusBadRequest)
return
}
apiKey, err := strconv.Atoi(idStr)
apiKeyID, err := strconv.Atoi(idStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("DeleteApiKeyHandler: %w", err)).Msg("Invalid API key ID")
utils.WriteError(w, "id is invalid", http.StatusBadRequest)
l.Debug().AnErr("error", err).Msg("DeleteApiKeyHandler: Invalid API key ID")
utils.WriteError(w, "invalid id", http.StatusBadRequest)
return
}
l.Debug().Msgf("DeleteApiKeyHandler: Deleting API key with ID: %d", apiKey)
err = store.DeleteApiKey(ctx, int32(apiKey))
if err != nil {
l.Err(fmt.Errorf("DeleteApiKeyHandler: %w", err)).Msg("Failed to delete API key")
if err := store.DeleteApiKey(ctx, int32(apiKeyID)); err != nil {
l.Error().Err(err).Msg("DeleteApiKeyHandler: Failed to delete API key")
utils.WriteError(w, "failed to delete api key", http.StatusInternalServerError)
return
}
l.Debug().Msgf("DeleteApiKeyHandler: Successfully deleted API key with ID: %d", apiKey)
l.Debug().Msgf("DeleteApiKeyHandler: Successfully deleted API key ID %d", apiKeyID)
w.WriteHeader(http.StatusNoContent)
}
}
@ -106,25 +104,23 @@ func GetApiKeysHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msgf("GetApiKeysHandler: Received request with params: '%s'", r.URL.Query().Encode())
l.Debug().Msg("GetApiKeysHandler: Received request")
user := middleware.GetUserFromContext(ctx)
if user == nil {
l.Debug().Msg("GetApiKeysHandler: Invalid user retrieved from context")
l.Debug().Msg("GetApiKeysHandler: Invalid user context")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
l.Debug().Msgf("GetApiKeysHandler: Retrieving API keys for user ID: %d", user.ID)
apiKeys, err := store.GetApiKeysByUserID(ctx, user.ID)
if err != nil {
l.Err(fmt.Errorf("GetApiKeysHandler: %w", err)).Msg("Failed to retrieve API keys")
l.Error().Err(err).Msg("GetApiKeysHandler: Failed to retrieve API keys")
utils.WriteError(w, "failed to retrieve api keys", http.StatusInternalServerError)
return
}
l.Debug().Msgf("GetApiKeysHandler: Successfully retrieved %d API keys for user ID: %d", len(apiKeys), user.ID)
l.Debug().Msgf("GetApiKeysHandler: Retrieved %d API keys", len(apiKeys))
utils.WriteJSON(w, http.StatusOK, apiKeys)
}
}
@ -134,45 +130,42 @@ func UpdateApiKeyLabelHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("UpdateApiKeyLabelHandler: Received request to update API key label")
l.Debug().Msg("UpdateApiKeyLabelHandler: Received request")
user := middleware.GetUserFromContext(ctx)
if user == nil {
l.Debug().Msg("UpdateApiKeyLabelHandler: Unauthorized request (user context is nil)")
l.Debug().Msg("UpdateApiKeyLabelHandler: Invalid user context")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
idStr := r.URL.Query().Get("id")
if idStr == "" {
l.Debug().Msg("UpdateApiKeyLabelHandler: Missing API key ID in request")
l.Debug().Msg("UpdateApiKeyLabelHandler: Missing id parameter")
utils.WriteError(w, "id is required", http.StatusBadRequest)
return
}
apiKeyID, err := strconv.Atoi(idStr)
if err != nil {
l.Debug().AnErr("error", fmt.Errorf("UpdateApiKeyLabelHandler: %w", err)).Msg("Invalid API key ID")
utils.WriteError(w, "id is invalid", http.StatusBadRequest)
l.Debug().AnErr("error", err).Msg("UpdateApiKeyLabelHandler: Invalid API key ID")
utils.WriteError(w, "invalid id", http.StatusBadRequest)
return
}
label := r.FormValue("label")
if label == "" {
l.Debug().Msg("UpdateApiKeyLabelHandler: Missing label in request")
l.Debug().Msg("UpdateApiKeyLabelHandler: Missing label parameter")
utils.WriteError(w, "label is required", http.StatusBadRequest)
return
}
l.Debug().Msgf("UpdateApiKeyLabelHandler: Updating label for API key ID %d", apiKeyID)
err = store.UpdateApiKeyLabel(ctx, db.UpdateApiKeyLabelOpts{
if err := store.UpdateApiKeyLabel(ctx, db.UpdateApiKeyLabelOpts{
UserID: user.ID,
ID: int32(apiKeyID),
Label: label,
})
if err != nil {
l.Err(fmt.Errorf("UpdateApiKeyLabelHandler: %w", err)).Msg("Failed to update API key label")
}); err != nil {
l.Error().Err(err).Msg("UpdateApiKeyLabelHandler: Failed to update API key label")
utils.WriteError(w, "failed to update api key label", http.StatusInternalServerError)
return
}

156
engine/handlers/artists.go Normal file
View file

@ -0,0 +1,156 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/internal/models"
"github.com/gabehf/koito/internal/utils"
)
func SetPrimaryArtistHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// sets the primary alias for albums, artists, and tracks
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("SetPrimaryArtistHandler: Got request")
r.ParseForm()
// Parse query parameters
artistIDStr := r.FormValue("artist_id")
albumIDStr := r.FormValue("album_id")
trackIDStr := r.FormValue("track_id")
isPrimaryStr := r.FormValue("is_primary")
l.Debug().Str("query", r.Form.Encode()).Msg("Recieved form")
if artistIDStr == "" {
l.Debug().Msg("SetPrimaryArtistHandler: artist_id must be provided")
utils.WriteError(w, "artist_id must be provided", http.StatusBadRequest)
return
}
if isPrimaryStr == "" {
l.Debug().Msg("SetPrimaryArtistHandler: is_primary must be provided")
utils.WriteError(w, "is_primary must be provided", http.StatusBadRequest)
return
}
primary, ok := utils.ParseBool(isPrimaryStr)
if !ok {
l.Debug().Msg("SetPrimaryArtistHandler: is_primary must be either true or false")
utils.WriteError(w, "is_primary must be either true or false", http.StatusBadRequest)
return
}
artistId, err := strconv.Atoi(artistIDStr)
if err != nil {
l.Debug().Msg("SetPrimaryArtistHandler: artist_id is invalid")
utils.WriteError(w, "artist_id is invalid", http.StatusBadRequest)
return
}
if albumIDStr == "" && trackIDStr == "" {
l.Debug().Msg("SetPrimaryArtistHandler: Missing album or track id parameter")
utils.WriteError(w, "album_id or track_id must be provided", http.StatusBadRequest)
return
}
if utils.MoreThanOneString(albumIDStr, trackIDStr) {
l.Debug().Msg("SetPrimaryArtistHandler: Multiple ID parameters provided")
utils.WriteError(w, "only one of album_id or track_id can be provided", http.StatusBadRequest)
return
}
if albumIDStr != "" {
id, err := strconv.Atoi(albumIDStr)
if err != nil {
l.Debug().AnErr("error", err).Msg("SetPrimaryArtistHandler: Invalid album id")
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
return
}
err = store.SetPrimaryAlbumArtist(ctx, int32(id), int32(artistId), primary)
if err != nil {
l.Error().Err(err).Msg("SetPrimaryArtistHandler: Failed to set album primary alias")
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
return
}
} else if trackIDStr != "" {
id, err := strconv.Atoi(trackIDStr)
if err != nil {
l.Debug().AnErr("error", err).Msg("SetPrimaryArtistHandler: Invalid track id")
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
return
}
err = store.SetPrimaryTrackArtist(ctx, int32(id), int32(artistId), primary)
if err != nil {
l.Error().Err(err).Msg("SetPrimaryArtistHandler: Failed to set track primary alias")
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
return
}
}
w.WriteHeader(http.StatusNoContent)
}
}
func GetArtistsForItemHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("GetArtistsForItemHandler: Received request to retrieve artists for item")
albumIDStr := r.URL.Query().Get("album_id")
trackIDStr := r.URL.Query().Get("track_id")
if albumIDStr == "" && trackIDStr == "" {
l.Debug().Msg("GetArtistsForItemHandler: Missing album or track ID parameter")
utils.WriteError(w, "album_id or track_id must be provided", http.StatusBadRequest)
return
}
if utils.MoreThanOneString(albumIDStr, trackIDStr) {
l.Debug().Msg("GetArtistsForItemHandler: Multiple ID parameters provided")
utils.WriteError(w, "only one of album_id or track_id can be provided", http.StatusBadRequest)
return
}
var artists []*models.Artist
var err error
if albumIDStr != "" {
albumID, convErr := strconv.Atoi(albumIDStr)
if convErr != nil {
l.Debug().AnErr("error", convErr).Msg("GetArtistsForItemHandler: Invalid album ID")
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
return
}
l.Debug().Msgf("GetArtistsForItemHandler: Fetching artists for album ID %d", albumID)
artists, err = store.GetArtistsForAlbum(ctx, int32(albumID))
} else if trackIDStr != "" {
trackID, convErr := strconv.Atoi(trackIDStr)
if convErr != nil {
l.Debug().AnErr("error", convErr).Msg("GetArtistsForItemHandler: Invalid track ID")
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
return
}
l.Debug().Msgf("GetArtistsForItemHandler: Fetching artists for track ID %d", trackID)
artists, err = store.GetArtistsForTrack(ctx, int32(trackID))
}
if err != nil {
l.Err(err).Msg("GetArtistsForItemHandler: Failed to retrieve artists")
utils.WriteError(w, "failed to retrieve artists", http.StatusInternalServerError)
return
}
l.Debug().Msg("GetArtistsForItemHandler: Successfully retrieved artists")
utils.WriteJSON(w, http.StatusOK, artists)
}
}

View file

@ -18,70 +18,62 @@ func LoginHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("LoginHandler: Received login request")
l.Debug().Msg("LoginHandler: Received request")
err := r.ParseForm()
if err != nil {
l.Debug().Msg("LoginHandler: Failed to parse request form")
utils.WriteError(w, "failed to parse request", http.StatusInternalServerError)
if err := r.ParseForm(); err != nil {
l.Debug().AnErr("error", err).Msg("LoginHandler: Failed to parse form")
utils.WriteError(w, "invalid request format", http.StatusBadRequest)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
l.Debug().Msg("LoginHandler: Missing username or password")
utils.WriteError(w, "username and password are required", http.StatusBadRequest)
l.Debug().Msg("LoginHandler: Missing credentials")
utils.WriteError(w, "username and password required", http.StatusBadRequest)
return
}
l.Debug().Msgf("LoginHandler: Searching for user with username '%s'", username)
user, err := store.GetUserByUsername(ctx, username)
if err != nil {
l.Err(err).Msg("LoginHandler: Error searching for user in database")
utils.WriteError(w, "internal server error", http.StatusInternalServerError)
l.Error().Err(err).Msg("LoginHandler: Database error fetching user")
utils.WriteError(w, "authentication failed", http.StatusInternalServerError)
return
} else if user == nil {
l.Debug().Msg("LoginHandler: Username or password is incorrect")
utils.WriteError(w, "username or password is incorrect", http.StatusBadRequest)
}
if user == nil {
l.Debug().Msg("LoginHandler: User not found")
utils.WriteError(w, "invalid credentials", http.StatusUnauthorized)
return
}
err = bcrypt.CompareHashAndPassword(user.Password, []byte(password))
if err != nil {
l.Debug().Msg("LoginHandler: Password comparison failed")
utils.WriteError(w, "username or password is incorrect", http.StatusBadRequest)
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(password)); err != nil {
l.Debug().Msg("LoginHandler: Invalid password")
utils.WriteError(w, "invalid credentials", http.StatusUnauthorized)
return
}
keepSignedIn := false
expiresAt := time.Now().Add(1 * 24 * time.Hour)
expiresAt := time.Now().Add(24 * time.Hour)
if strings.ToLower(r.FormValue("remember_me")) == "true" {
keepSignedIn = true
expiresAt = time.Now().Add(30 * 24 * time.Hour)
}
l.Debug().Msgf("LoginHandler: Creating session for user ID %d", user.ID)
session, err := store.SaveSession(ctx, user.ID, expiresAt, keepSignedIn)
session, err := store.SaveSession(ctx, user.ID, expiresAt, r.FormValue("remember_me") == "true")
if err != nil {
l.Err(err).Msg("LoginHandler: Failed to create session")
utils.WriteError(w, "failed to create session", http.StatusInternalServerError)
l.Error().Err(err).Msg("LoginHandler: Failed to create session")
utils.WriteError(w, "authentication failed", http.StatusInternalServerError)
return
}
cookie := &http.Cookie{
http.SetCookie(w, &http.Cookie{
Name: "koito_session",
Value: session.ID.String(),
Expires: expiresAt,
Path: "/",
HttpOnly: true,
Secure: false,
}
})
if keepSignedIn {
cookie.Expires = expiresAt
}
l.Debug().Msgf("LoginHandler: Session created successfully for user ID %d", user.ID)
http.SetCookie(w, cookie)
l.Debug().Msgf("LoginHandler: User %d authenticated", user.ID)
w.WriteHeader(http.StatusNoContent)
}
}
@ -91,34 +83,27 @@ func LogoutHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("LogoutHandler: Received logout request")
l.Debug().Msg("LogoutHandler: Received request")
cookie, err := r.Cookie("koito_session")
if err == nil {
l.Debug().Msg("LogoutHandler: Found session cookie")
sid, err := uuid.Parse(cookie.Value)
if err != nil {
l.Debug().AnErr("error", err).Msg("LogoutHandler: Invalid session cookie")
utils.WriteError(w, "session cookie is invalid", http.StatusUnauthorized)
return
}
l.Debug().Msgf("LogoutHandler: Deleting session with ID %s", sid)
err = store.DeleteSession(ctx, sid)
if err != nil {
l.Err(err).Msg("LogoutHandler: Failed to delete session")
utils.WriteError(w, "internal server error", http.StatusInternalServerError)
return
l.Debug().AnErr("error", err).Msg("LogoutHandler: Invalid session ID")
} else if err := store.DeleteSession(ctx, sid); err != nil {
l.Error().Err(err).Msg("LogoutHandler: Failed to delete session")
}
}
l.Debug().Msg("LogoutHandler: Clearing session cookie")
http.SetCookie(w, &http.Cookie{
Name: "koito_session",
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1, // expire immediately
MaxAge: -1,
})
l.Debug().Msg("LogoutHandler: Session terminated")
w.WriteHeader(http.StatusNoContent)
}
}
@ -128,16 +113,17 @@ func MeHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("MeHandler: Received request to retrieve user information")
u := middleware.GetUserFromContext(ctx)
if u == nil {
l.Debug().Msg("MeHandler: Invalid user retrieved from context")
l.Debug().Msg("MeHandler: Received request")
user := middleware.GetUserFromContext(ctx)
if user == nil {
l.Debug().Msg("MeHandler: Unauthorized access")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
l.Debug().Msgf("MeHandler: Successfully retrieved user with ID %d", u.ID)
utils.WriteJSON(w, http.StatusOK, u)
l.Debug().Msgf("MeHandler: Returning user data for ID %d", user.ID)
utils.WriteJSON(w, http.StatusOK, user)
}
}
@ -146,41 +132,42 @@ func UpdateUserHandler(store db.DB) http.HandlerFunc {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("UpdateUserHandler: Received request to update user information")
u := middleware.GetUserFromContext(ctx)
if u == nil {
l.Debug().Msg("UpdateUserHandler: Unauthorized request (user context is nil)")
l.Debug().Msg("UpdateUserHandler: Received request")
user := middleware.GetUserFromContext(ctx)
if user == nil {
l.Debug().Msg("UpdateUserHandler: Unauthorized access")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
err := r.ParseForm()
if err != nil {
l.Err(err).Msg("UpdateUserHandler: Failed to parse request form")
utils.WriteError(w, "failed to parse request", http.StatusInternalServerError)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" && password == "" {
l.Debug().Msg("UpdateUserHandler: No parameters were recieved")
utils.WriteError(w, "all parameters missing", http.StatusBadRequest)
return
}
l.Debug().Msgf("UpdateUserHandler: Updating user with ID %d", u.ID)
err = store.UpdateUser(ctx, db.UpdateUserOpts{
ID: u.ID,
Username: username,
Password: password,
})
if err != nil {
l.Err(err).Msg("UpdateUserHandler: Failed to update user")
utils.WriteError(w, err.Error(), http.StatusBadRequest)
if err := r.ParseForm(); err != nil {
l.Error().Err(err).Msg("UpdateUserHandler: Invalid form data")
utils.WriteError(w, "invalid request", http.StatusBadRequest)
return
}
l.Debug().Msgf("UpdateUserHandler: Successfully updated user with ID %d", u.ID)
opts := db.UpdateUserOpts{ID: user.ID}
if username := r.FormValue("username"); username != "" {
opts.Username = username
}
if password := r.FormValue("password"); password != "" {
opts.Password = password
}
if opts.Username == "" && opts.Password == "" {
l.Debug().Msg("UpdateUserHandler: No update parameters provided")
utils.WriteError(w, "no changes specified", http.StatusBadRequest)
return
}
if err := store.UpdateUser(ctx, opts); err != nil {
l.Error().Err(err).Msg("UpdateUserHandler: Update failed")
utils.WriteError(w, "update failed", http.StatusBadRequest)
return
}
l.Debug().Msgf("UpdateUserHandler: User %d updated", user.ID)
w.WriteHeader(http.StatusNoContent)
}
}

View file

@ -10,7 +10,6 @@ import (
"github.com/gabehf/koito/internal/utils"
)
// DeleteTrackHandler deletes a track by its ID.
func DeleteTrackHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -46,7 +45,6 @@ func DeleteTrackHandler(store db.DB) http.HandlerFunc {
}
}
// DeleteListenHandler deletes a listen record by track ID and timestamp.
func DeleteListenHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -96,7 +94,6 @@ func DeleteListenHandler(store db.DB) http.HandlerFunc {
}
}
// DeleteArtistHandler deletes an artist by its ID.
func DeleteArtistHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -132,7 +129,6 @@ func DeleteArtistHandler(store db.DB) http.HandlerFunc {
}
}
// DeleteAlbumHandler deletes an album by its ID.
func DeleteAlbumHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View file

@ -117,7 +117,12 @@ func serveDefaultImage(w http.ResponseWriter, r *http.Request, size catalog.Imag
return
}
lock.Lock()
utils.CopyFile(path.Join("assets", "default_img"), defaultImagePath)
err = utils.CopyFile(path.Join("assets", "default_img"), defaultImagePath)
if err != nil {
l.Err(err).Msg("serveDefaultImage: Error when copying default image from assets")
w.WriteHeader(http.StatusInternalServerError)
return
}
lock.Unlock()
} else if err != nil {
l.Err(err).Msg("serveDefaultImage: Error when attempting to read default image in cache")
@ -151,7 +156,7 @@ func serveDefaultImage(w http.ResponseWriter, r *http.Request, size catalog.Imag
func downloadMissingImage(ctx context.Context, store db.DB, id uuid.UUID) (string, error) {
src, err := store.GetImageSource(ctx, id)
if err != nil {
return "", fmt.Errorf("downloadMissingImage: store.GetImageSource: %w", err)
return "", fmt.Errorf("downloadMissingImage: %w", err)
}
var size catalog.ImageSize
if cfg.FullImageCacheEnabled() {
@ -161,7 +166,7 @@ func downloadMissingImage(ctx context.Context, store db.DB, id uuid.UUID) (strin
}
err = catalog.DownloadAndCacheImage(ctx, id, src, size)
if err != nil {
return "", fmt.Errorf("downloadMissingImage: catalog.DownloadAndCacheImage: %w", err)
return "", fmt.Errorf("downloadMissingImage: %w", err)
}
return path.Join(catalog.SourceImageDir(), id.String()), nil
}

View file

@ -137,13 +137,13 @@ func LbzSubmitListenHandler(store db.DB, mbzc mbz.MusicBrainzCaller) func(w http
artistMbzIDs, err := utils.ParseUUIDSlice(payload.TrackMeta.AdditionalInfo.ArtistMBIDs)
if err != nil {
l.Debug().Err(err).Msg("LbzSubmitListenHandler: Failed to parse one or more UUIDs")
l.Debug().AnErr("error", err).Msg("LbzSubmitListenHandler: Failed to parse one or more UUIDs")
}
if len(artistMbzIDs) < 1 {
l.Debug().Err(err).Msg("LbzSubmitListenHandler: Attempting to parse artist UUIDs from mbid_mapping")
l.Debug().AnErr("error", err).Msg("LbzSubmitListenHandler: Attempting to parse artist UUIDs from mbid_mapping")
utils.ParseUUIDSlice(payload.TrackMeta.MBIDMapping.ArtistMBIDs)
if err != nil {
l.Debug().Err(err).Msg("LbzSubmitListenHandler: Failed to parse one or more UUIDs")
l.Debug().AnErr("error", err).Msg("LbzSubmitListenHandler: Failed to parse one or more UUIDs")
}
}
rgMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.ReleaseGroupMBID)
@ -191,7 +191,7 @@ func LbzSubmitListenHandler(store db.DB, mbzc mbz.MusicBrainzCaller) func(w http
}
mbid, err := uuid.Parse(a.ArtistMBID)
if err != nil {
l.Err(err).Msgf("LbzSubmitListenHandler: Failed to parse UUID for artist '%s'", a.ArtistName)
l.Debug().AnErr("error", err).Msgf("LbzSubmitListenHandler: Failed to parse UUID for artist '%s'", a.ArtistName)
}
artistMbidMap = append(artistMbidMap, catalog.ArtistMbidMap{Artist: a.ArtistName, Mbid: mbid})
}

View file

@ -53,6 +53,7 @@ func makeAuthRequest(t *testing.T, session, method, endpoint string, body io.Rea
Name: "koito_session",
Value: session,
})
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
t.Logf("Making request to %s with session: %s", endpoint, session)
return http.DefaultClient.Do(req)
}
@ -512,7 +513,7 @@ func TestAuth(t *testing.T) {
encoded = formdata.Encode()
resp, err = http.DefaultClient.Post(host()+"/apis/web/v1/login", "application/x-www-form-urlencoded", strings.NewReader(encoded))
require.NoError(t, err)
require.Equal(t, 400, resp.StatusCode)
require.Equal(t, 401, resp.StatusCode)
// reset update so other tests dont fail
req, err = http.NewRequest("PATCH", host()+fmt.Sprintf("/apis/web/v1/user?username=%s&password=%s", cfg.DefaultUsername(), cfg.DefaultPassword()), nil)
@ -732,3 +733,160 @@ func TestAlbumReplaceImage(t *testing.T) {
assert.NotNil(t, a.Image)
assert.Equal(t, newid, *a.Image)
}
func TestSetPrimaryArtist(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
ctx := context.Background()
// set and unset track primary artist
formdata := url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("track_id", "1")
formdata.Set("is_primary", "false")
body := formdata.Encode()
resp, err := makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err := store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_tracks
WHERE track_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, false)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be false")
formdata = url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("track_id", "1")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_tracks
WHERE track_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, true)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be true")
// set and unset album primary artist
formdata = url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("album_id", "1")
formdata.Set("is_primary", "false")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_releases
WHERE release_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, false)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be false")
formdata = url.Values{}
formdata.Set("artist_id", "1")
formdata.Set("album_id", "1")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_releases
WHERE release_id = $1 AND artist_id = $2 AND is_primary = $3
)`, 1, 1, true)
require.NoError(t, err)
assert.True(t, exists, "expected artist is_primary to be true")
// create a new track with multiple artists to make sure only one is primary at a time
listenBody := `{
"listen_type": "single",
"payload": [
{
"listened_at": 1749475719,
"track_metadata": {
"additional_info": {
"artist_names": [
"Rat Tally",
"Madeline Kenney"
],
"duration_ms": 197270,
"submission_client": "navidrome",
"submission_client_version": "0.56.1 (fa2cf362)"
},
"artist_name": "Rat Tally feat. Madeline Kenney",
"release_name": "In My Car",
"track_name": "In My Car"
}
}
]
}`
req, err := http.NewRequest("POST", host()+"/apis/listenbrainz/1/submit-listens", strings.NewReader(listenBody))
require.NoError(t, err)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", apikey))
req.Header.Add("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
respBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"status": "ok"}`, string(respBytes))
// set both artists as primary
formdata = url.Values{}
formdata.Set("artist_id", "4")
formdata.Set("album_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
formdata = url.Values{}
formdata.Set("artist_id", "5")
formdata.Set("album_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
formdata = url.Values{}
formdata.Set("artist_id", "4")
formdata.Set("track_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
formdata = url.Values{}
formdata.Set("artist_id", "5")
formdata.Set("track_id", "4")
formdata.Set("is_primary", "true")
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/artists/primary", strings.NewReader(body))
require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode)
count, err := store.Count(ctx, `SELECT COUNT(*) FROM artist_releases WHERE release_id = $1 AND is_primary = $2`, 4, true)
require.NoError(t, err)
assert.EqualValues(t, 1, count, "expected only one primary artist for release")
count, err = store.Count(ctx, `SELECT COUNT(*) FROM artist_tracks WHERE track_id = $1 AND is_primary = $2`, 4, true)
require.NoError(t, err)
assert.EqualValues(t, 1, count, "expected only one primary artist for track")
}

View file

@ -36,6 +36,7 @@ func bindRoutes(
r.Route("/apis/web/v1", func(r chi.Router) {
r.Get("/artist", handlers.GetArtistHandler(db))
r.Get("/artists", handlers.GetArtistsForItemHandler(db))
r.Get("/album", handlers.GetAlbumHandler(db))
r.Get("/track", handlers.GetTrackHandler(db))
r.Get("/top-tracks", handlers.GetTopTracksHandler(db))
@ -75,6 +76,7 @@ func bindRoutes(
r.Post("/merge/albums", handlers.MergeReleaseGroupsHandler(db))
r.Post("/merge/artists", handlers.MergeArtistsHandler(db))
r.Delete("/artist", handlers.DeleteArtistHandler(db))
r.Post("/artists/primary", handlers.SetPrimaryArtistHandler(db))
r.Delete("/album", handlers.DeleteAlbumHandler(db))
r.Delete("/track", handlers.DeleteTrackHandler(db))
r.Delete("/listen", handlers.DeleteListenHandler(db))