feat(artist): editable display name + rename surface; drop name-uniqueness (#130 step 1)
First step of decoupling artist identity/storage/display. migration 0077 drops uq_artist_name so the display name is free text (two genuinely different creators can share a name); the slug stays the immutable, unique storage/identity key (the on-disk path component — untouched, so nothing moves). ArtistService.rename + PATCH /api/artists/<id> change the name ONLY. Frontend: inline pencil-edit on the artist header (mirrors TagCard), slug/route unaffected so no navigation. Fixes the operator's 'no surface to rename an artist' + the name-collision fragility. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -283,6 +283,23 @@ class ArtistService:
|
||||
await self.session.commit()
|
||||
return artist, created
|
||||
|
||||
async def rename(self, artist_id: int, name: str) -> Artist | None:
|
||||
"""Change the display NAME only (#130). The slug — and every on-disk path
|
||||
keyed off it — is IMMUTABLE, so a rename never moves files or risks a path
|
||||
collision. Name is free text and NON-unique (migration 0077). Returns the
|
||||
updated Artist, None if not found; raises ValueError on empty name."""
|
||||
cleaned = (name or "").strip()
|
||||
if not cleaned:
|
||||
raise ValueError("artist name must not be empty")
|
||||
artist = (await self.session.execute(
|
||||
select(Artist).where(Artist.id == artist_id)
|
||||
)).scalar_one_or_none()
|
||||
if artist is None:
|
||||
return None
|
||||
artist.name = cleaned
|
||||
await self.session.commit()
|
||||
return artist
|
||||
|
||||
async def autocomplete(self, prefix: str, limit: int = 20) -> list[Artist]:
|
||||
cleaned = (prefix or "").strip()
|
||||
if not cleaned:
|
||||
|
||||
Reference in New Issue
Block a user