commit 03b390e3b3ba0d737f9cf891b44eb9c6098a42c7
parent 1da8e98eca8f0e682cfeb5c63aeac5e51e0af938
Author: brookjeynes <me@brookjeynes.dev>
Date: Sun, 3 May 2026 21:38:55 +1000
feat: add disambiguation field for artists
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
10 files changed, 88 insertions(+), 25 deletions(-)
diff --git a/internal/db/artist.go b/internal/db/artist.go
@@ -12,11 +12,12 @@ func InsertArtist(e Execer, artist domain.Artist) error {
insert into artists (
name,
latin_name,
+ disambiguation,
bio,
created_at
)
- values (?, ?, ?, ?)
- `, artist.Name, artist.LatinName, artist.Bio, time.Now().Format(time.RFC3339))
+ values (?, ?, ?, ?, ?)
+ `, artist.Name, artist.LatinName, artist.Disambiguation, artist.Bio, time.Now().Format(time.RFC3339))
if err != nil {
return fmt.Errorf("failed to insert artist: %w", err)
}
@@ -30,9 +31,10 @@ func UpdateArtist(e Execer, artist domain.Artist) error {
set
name = ?,
latin_name = ?,
+ disambiguation = ?,
bio = ?
where id = ?
- `, artist.Name, artist.LatinName, artist.Bio, artist.ID)
+ `, artist.Name, artist.LatinName, artist.Disambiguation, artist.Bio, artist.ID)
if err != nil {
return fmt.Errorf("failed to update artist: %w", err)
}
@@ -48,7 +50,7 @@ func SearchArtist(e Execer, artistName string) ([]domain.Artist, error) {
wildcard := "%" + artistName + "%"
rows, err := e.Query(`
- select id, name, latin_name, bio
+ select id, name, latin_name, disambiguation, bio
from artists
where name like ? collate nocase
or latin_name like ? collate nocase
@@ -63,7 +65,7 @@ func SearchArtist(e Execer, artistName string) ([]domain.Artist, error) {
var results []domain.Artist
for rows.Next() {
var artist domain.Artist
- if err := rows.Scan(&artist.ID, &artist.Name, &artist.LatinName, &artist.Bio); err != nil {
+ if err := rows.Scan(&artist.ID, &artist.Name, &artist.LatinName, &artist.Disambiguation, &artist.Bio); err != nil {
return nil, fmt.Errorf("failed to scan artist row: %w", err)
}
results = append(results, artist)
diff --git a/internal/db/db.go b/internal/db/db.go
@@ -77,6 +77,7 @@ func Make(ctx context.Context, dbPath string) (*DB, error) {
id integer primary key autoincrement,
name text not null,
latin_name text,
+ disambiguation text,
bio text,
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
diff --git a/internal/domain/artist.go b/internal/domain/artist.go
@@ -1,8 +1,9 @@
package domain
type Artist struct {
- ID int64
- Name string
- LatinName *string
- Bio *string
+ ID int64
+ Name string
+ LatinName *string
+ Disambiguation *string
+ Bio *string
}
diff --git a/internal/server/new-artist.go b/internal/server/new-artist.go
@@ -10,9 +10,10 @@ import (
)
type NewArtistForm struct {
- Name string `schema:"artist"`
- Bio string `schema:"biography"`
- LatinName string `schema:"latin-name"`
+ Name string `schema:"artist"`
+ Bio string `schema:"biography"`
+ LatinName string `schema:"latin-name"`
+ Disambiguation string `schema:"disambiguation"`
}
func parseNewArtistForm(form NewArtistForm) (domain.Artist, error) {
@@ -33,10 +34,16 @@ func parseNewArtistForm(form NewArtistForm) (domain.Artist, error) {
return artist, fmt.Errorf("Artist biography must not be empty or have leading/trailing whitespace")
}
+ // disambiguation
+ if form.Disambiguation != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Disambiguation) {
+ return artist, fmt.Errorf("Artist disambiguation must not be empty or have leading/trailing whitespace")
+ }
+
artist = domain.Artist{
- Name: form.Name,
- LatinName: &form.LatinName,
- Bio: &form.Bio,
+ Name: form.Name,
+ LatinName: &form.LatinName,
+ Bio: &form.Bio,
+ Disambiguation: &form.Disambiguation,
}
return artist, nil
diff --git a/internal/server/new-song.go b/internal/server/new-song.go
@@ -33,14 +33,17 @@ func parseNewSongForm(form NewSongForm) (domain.Song, error) {
}
}
+ // title
if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) {
return song, fmt.Errorf("Title must not be empty or have leading/trailing whitespace")
}
+ // latin title
if form.LatinTitle != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.LatinTitle) {
return song, fmt.Errorf("Latin title must not be empty")
}
+ // lyrics
if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Lyrics) {
return song, fmt.Errorf("Lyrics must not be empty")
}
diff --git a/internal/server/search-artist.go b/internal/server/search-artist.go
@@ -5,7 +5,13 @@ import (
artistsearchdatalist "lyrics/internal/server/ui/partials/artist-search-datalist"
similarartistslist "lyrics/internal/server/ui/partials/similar-artists-list"
"net/http"
- "strings"
+)
+
+type SearchArtistFormat string
+
+const (
+ SearchArtistFormatSimilar SearchArtistFormat = "similar"
+ SearchArtistFormatSuggest SearchArtistFormat = "suggest"
)
func (ss *ServerState) SearchArtist(w http.ResponseWriter, r *http.Request) {
@@ -13,6 +19,12 @@ func (ss *ServerState) SearchArtist(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
+ format := SearchArtistFormat(r.URL.Query().Get("format"))
+ if format != SearchArtistFormatSimilar && format != SearchArtistFormatSuggest {
+ http.Error(w, "invalid format param", http.StatusBadRequest)
+ return
+ }
+
artist := r.URL.Query().Get("artist")
results, err := db.SearchArtist(ss.db, artist)
@@ -21,12 +33,12 @@ func (ss *ServerState) SearchArtist(w http.ResponseWriter, r *http.Request) {
return
}
- requestFromUrl := r.Header.Get("HX-Current-URL")
- if strings.Contains(requestFromUrl, "/artist/new") {
+ switch format {
+ case SearchArtistFormatSimilar:
similarartistslist.SimilarArtistsList(similarartistslist.SimilarArtistsListParams{
Artists: results,
}).Render(r.Context(), w)
- } else {
+ case SearchArtistFormatSuggest:
artistsearchdatalist.ArtistSearchDatalist(artistsearchdatalist.ArtistSearchDatalistParams{
Artists: results,
}).Render(r.Context(), w)
diff --git a/internal/server/ui/partials/artist-search-datalist/artist-search-datalist.templ b/internal/server/ui/partials/artist-search-datalist/artist-search-datalist.templ
@@ -1,9 +1,22 @@
package artistsearchdatalist
+import "fmt"
+
templ ArtistSearchDatalist(params ArtistSearchDatalistParams) {
<datalist id="artist-suggestions">
- for _, a := range params.Artists {
- <option value={ a.Name }></option>
+ for _, artist := range params.Artists {
+ {{
+ value := artist.Name
+
+ if artist.LatinName != nil && artist.Disambiguation != nil {
+ value = fmt.Sprintf("%s (%s, %s)", artist.Name, *artist.LatinName, *artist.Disambiguation)
+ } else if artist.LatinName != nil {
+ value = fmt.Sprintf("%s (%s)", artist.Name, *artist.LatinName)
+ } else if artist.Disambiguation != nil {
+ value = fmt.Sprintf("%s (%s)", artist.Name, *artist.Disambiguation)
+ }
+ }}
+ <option value={ value }></option>
}
</datalist>
}
diff --git a/internal/server/ui/partials/similar-artists-list/similar-artists-list.templ b/internal/server/ui/partials/similar-artists-list/similar-artists-list.templ
@@ -1,9 +1,24 @@
package similarartistslist
+import "fmt"
+
templ SimilarArtistsList(params SimilarArtistsListParams) {
<ul id="similar-artists">
- for _, a := range params.Artists {
- <li>{ a.Name }</li>
+ for _, artist := range params.Artists {
+ {{
+ value := artist.Name
+
+ if artist.LatinName != nil && artist.Disambiguation != nil {
+ value = fmt.Sprintf("%s (%s, %s)", artist.Name, *artist.LatinName, *artist.Disambiguation)
+ } else if artist.LatinName != nil {
+ value = fmt.Sprintf("%s (%s)", artist.Name, *artist.LatinName)
+ } else if artist.Disambiguation != nil {
+ value = fmt.Sprintf("%s (%s)", artist.Name, *artist.Disambiguation)
+ }
+ }}
+ <li title={ value }>
+ { value }
+ </li>
}
</ul>
}
diff --git a/internal/server/ui/views/new-artist/new-artist.templ b/internal/server/ui/views/new-artist/new-artist.templ
@@ -17,7 +17,7 @@ templ NewArtist(params NewArtistParams) {
<label>
artist:
<input
- hx-get="/artist/search"
+ hx-get="/artist/search?format=similar"
hx-trigger="input changed delay:500ms, load"
hx-target="#similar-artists"
hx-swap="outerHTML"
@@ -38,6 +38,15 @@ templ NewArtist(params NewArtistParams) {
/>
</label>
</div>
+ <label title="helps users distinguish between artists with identical or similar names, e.g. South Korean singer-songwriter">
+ disambiguation:
+ <input
+ name="disambiguation"
+ type="text"
+ pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
+ title="helps users distinguish between artists with identical or similar names, e.g. South Korean singer-songwriter"
+ />
+ </label>
<div>
<span>similar artists</span>
<ul id="similar-artists"></ul>
diff --git a/internal/server/ui/views/new-song/new-song.templ b/internal/server/ui/views/new-song/new-song.templ
@@ -26,7 +26,7 @@ templ NewSong(params NewSongParams) {
<label>
<span>artist</span>
<input
- hx-get="/artist/search"
+ hx-get="/artist/search?format=suggest"
hx-trigger="input changed delay:500ms, load"
hx-target="#artist-suggestions"
hx-swap="outerHTML"