feat: add edit artist endpoint (section 1.4)

- Add GET/PUT /api/artists/{id} for artist settings
- Update sync to create artists table entry (library settings)
- Support partial updates for monitored, path, quality/metadata profiles
- Add e2e tests for get, edit, partial update flows
This commit is contained in:
Alexander
2026-04-29 13:22:14 +02:00
parent b08a0b1646
commit ff49403fd5
6 changed files with 423 additions and 14 deletions
+48
View File
@@ -323,6 +323,54 @@ func (h *Handlers) DeleteArtist(w http.ResponseWriter, r *http.Request) {
})
}
func (h *Handlers) GetArtist(w http.ResponseWriter, r *http.Request) {
if h.DB == nil {
writeError(w, http.StatusServiceUnavailable, "database not connected")
return
}
artistID := chi.URLParam(r, "id")
if artistID == "" {
writeError(w, http.StatusBadRequest, "artist ID required")
return
}
artist, err := h.DB.GetArtistByForeignID(r.Context(), artistID)
if err != nil {
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
return
}
writeJSON(w, http.StatusOK, artist)
}
func (h *Handlers) EditArtist(w http.ResponseWriter, r *http.Request) {
if h.DB == nil {
writeError(w, http.StatusServiceUnavailable, "database not connected")
return
}
artistID := chi.URLParam(r, "id")
if artistID == "" {
writeError(w, http.StatusBadRequest, "artist ID required")
return
}
var update database.ArtistUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
artist, err := h.DB.UpdateArtistByForeignID(r.Context(), artistID, update)
if err != nil {
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
return
}
writeJSON(w, http.StatusOK, artist)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
+2
View File
@@ -44,6 +44,8 @@ func NewRouter(h *Handlers) *chi.Mux {
r.Post("/sync", h.Sync)
r.Route("/artists", func(r chi.Router) {
r.Get("/{id}", h.GetArtist)
r.Put("/{id}", h.EditArtist)
r.Post("/{id}/refresh", h.RefreshArtist)
r.Delete("/{id}", h.DeleteArtist)
})
+141 -13
View File
@@ -251,19 +251,6 @@ func (db *DB) CountAlbums(ctx context.Context) (int64, error) {
return count, err
}
func (db *DB) GetArtistByForeignID(ctx context.Context, foreignArtistID string) (*ArtistMetadataRow, error) {
var a ArtistMetadataRow
err := db.pool.QueryRow(ctx, `
SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at
FROM artist_metadata
WHERE foreign_artist_id = $1
`, foreignArtistID).Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, err
}
return &a, nil
}
func (db *DB) CountAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) (int64, error) {
var count int64
err := db.pool.QueryRow(ctx, `
@@ -288,3 +275,144 @@ func (db *DB) DeleteArtistByForeignID(ctx context.Context, foreignArtistID strin
}
return result.RowsAffected() > 0, nil
}
type ArtistRow struct {
ID uuid.UUID `json:"id"`
MetadataID uuid.UUID `json:"metadata_id"`
ForeignArtistID string `json:"foreign_artist_id"`
Name string `json:"name"`
QualityProfileID *uuid.UUID `json:"quality_profile_id"`
MetadataProfileID *uuid.UUID `json:"metadata_profile_id"`
RootFolderID *uuid.UUID `json:"root_folder_id"`
Path *string `json:"path"`
Monitored bool `json:"monitored"`
MonitorNewItems string `json:"monitor_new_items"`
}
func (db *DB) UpsertArtist(ctx context.Context, metadataID uuid.UUID) (uuid.UUID, error) {
var existingID uuid.UUID
err := db.pool.QueryRow(ctx, `
SELECT id FROM artists WHERE metadata_id = $1
`, metadataID).Scan(&existingID)
if err == nil {
return existingID, nil
}
var resultID uuid.UUID
err = db.pool.QueryRow(ctx, `
INSERT INTO artists (metadata_id, monitored, monitor_new_items)
VALUES ($1, true, 'all')
RETURNING id
`, metadataID).Scan(&resultID)
return resultID, err
}
func (db *DB) GetArtistByForeignID(ctx context.Context, foreignArtistID string) (*ArtistRow, error) {
var a ArtistRow
err := db.pool.QueryRow(ctx, `
SELECT a.id, a.metadata_id, am.foreign_artist_id, am.name,
a.quality_profile_id, a.metadata_profile_id, a.root_folder_id,
a.path, a.monitored, a.monitor_new_items
FROM artists a
JOIN artist_metadata am ON a.metadata_id = am.id
WHERE am.foreign_artist_id = $1
`, foreignArtistID).Scan(
&a.ID, &a.MetadataID, &a.ForeignArtistID, &a.Name,
&a.QualityProfileID, &a.MetadataProfileID, &a.RootFolderID,
&a.Path, &a.Monitored, &a.MonitorNewItems,
)
if err != nil {
return nil, err
}
return &a, nil
}
type ArtistUpdate struct {
QualityProfileID *string `json:"quality_profile_id"`
MetadataProfileID *string `json:"metadata_profile_id"`
RootFolderID *string `json:"root_folder_id"`
Path *string `json:"path"`
Monitored *bool `json:"monitored"`
MonitorNewItems *string `json:"monitor_new_items"`
}
func (db *DB) UpdateArtistByForeignID(ctx context.Context, foreignArtistID string, update ArtistUpdate) (*ArtistRow, error) {
metadataRow, err := db.GetArtistMetadataByForeignID(ctx, foreignArtistID)
if err != nil {
return nil, err
}
if update.Monitored != nil {
_, err = db.pool.Exec(ctx, `
UPDATE artists SET monitored = $1 WHERE metadata_id = $2
`, *update.Monitored, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.Path != nil {
_, err = db.pool.Exec(ctx, `
UPDATE artists SET path = $1 WHERE metadata_id = $2
`, *update.Path, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.QualityProfileID != nil {
var qpID *uuid.UUID
if *update.QualityProfileID != "" {
parsed, err := uuid.Parse(*update.QualityProfileID)
if err == nil {
qpID = &parsed
}
}
_, err = db.pool.Exec(ctx, `
UPDATE artists SET quality_profile_id = $1 WHERE metadata_id = $2
`, qpID, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.RootFolderID != nil {
var rfID *uuid.UUID
if *update.RootFolderID != "" {
parsed, err := uuid.Parse(*update.RootFolderID)
if err == nil {
rfID = &parsed
}
}
_, err = db.pool.Exec(ctx, `
UPDATE artists SET root_folder_id = $1 WHERE metadata_id = $2
`, rfID, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.MonitorNewItems != nil {
_, err = db.pool.Exec(ctx, `
UPDATE artists SET monitor_new_items = $1 WHERE metadata_id = $2
`, *update.MonitorNewItems, metadataRow.ID)
if err != nil {
return nil, err
}
}
return db.GetArtistByForeignID(ctx, foreignArtistID)
}
func (db *DB) GetArtistMetadataByForeignID(ctx context.Context, foreignArtistID string) (*ArtistMetadataRow, error) {
var a ArtistMetadataRow
err := db.pool.QueryRow(ctx, `
SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at
FROM artist_metadata
WHERE foreign_artist_id = $1
`, foreignArtistID).Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, err
}
return &a, nil
}
+5 -1
View File
@@ -101,6 +101,10 @@ func Sync(
idStr := id.String()
artistMetadataID = &idStr
log.Info().Str("artist", artist.Name).Str("id", idStr).Msg("stored artist metadata")
if _, err := db.UpsertArtist(ctx, id); err != nil {
log.Warn().Err(err).Str("artist", artist.Name).Msg("failed to create artist library entry")
}
}
}
@@ -306,7 +310,7 @@ func RefreshArtist(
return nil, &NotFoundError{Message: "database not available"}
}
existingArtist, err := db.GetArtistByForeignID(ctx, foreignArtistID)
existingArtist, err := db.GetArtistMetadataByForeignID(ctx, foreignArtistID)
if err != nil {
return nil, &NotFoundError{Message: "artist not found: " + foreignArtistID}
}