commit 1da8e98eca8f0e682cfeb5c63aeac5e51e0af938
parent c17077845bc162d631c074e7f1acfdac153f27ff
Author: brookjeynes <me@brookjeynes.dev>
Date: Sun, 3 May 2026 17:35:14 +1000
feat: add latin_name field
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
9 files changed, 104 insertions(+), 44 deletions(-)
diff --git a/internal/db/artist.go b/internal/db/artist.go
@@ -7,20 +7,34 @@ import (
"time"
)
-func UpsertArtist(e Execer, artist domain.Artist) error {
+func InsertArtist(e Execer, artist domain.Artist) error {
_, err := e.Exec(`
insert into artists (
name,
+ latin_name,
bio,
created_at
)
- values (?, ?, ?)
- on conflict(id) do update set
- name = excluded.name,
- bio = excluded.bio
- `, artist.Name, artist.Bio, time.Now().Format(time.RFC3339))
+ values (?, ?, ?, ?)
+ `, artist.Name, artist.LatinName, artist.Bio, time.Now().Format(time.RFC3339))
if err != nil {
- return fmt.Errorf("failed to insert or update artist: %w", err)
+ return fmt.Errorf("failed to insert artist: %w", err)
+ }
+
+ return nil
+}
+
+func UpdateArtist(e Execer, artist domain.Artist) error {
+ _, err := e.Exec(`
+ update artists
+ set
+ name = ?,
+ latin_name = ?,
+ bio = ?
+ where id = ?
+ `, artist.Name, artist.LatinName, artist.Bio, artist.ID)
+ if err != nil {
+ return fmt.Errorf("failed to update artist: %w", err)
}
return nil
@@ -32,13 +46,15 @@ func SearchArtist(e Execer, artistName string) ([]domain.Artist, error) {
return nil, nil
}
+ wildcard := "%" + artistName + "%"
rows, err := e.Query(`
- select id, name, bio
- from artists
- where name like ? collate nocase
- order by name
- limit 20
- `, "%"+artistName+"%")
+ select id, name, latin_name, bio
+ from artists
+ where name like ? collate nocase
+ or latin_name like ? collate nocase
+ order by name
+ limit 20
+ `, wildcard, wildcard)
if err != nil {
return nil, fmt.Errorf("failed to query artists: %w", err)
}
@@ -47,7 +63,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.Bio); err != nil {
+ if err := rows.Scan(&artist.ID, &artist.Name, &artist.LatinName, &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
@@ -62,6 +62,7 @@ func Make(ctx context.Context, dbPath string) (*DB, error) {
create table if not exists songs (
id integer primary key autoincrement,
title text not null,
+ latin_title text,
lyrics text not null,
release_date text not null,
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
@@ -75,6 +76,7 @@ func Make(ctx context.Context, dbPath string) (*DB, error) {
create table if not exists artists (
id integer primary key autoincrement,
name text not null,
+ latin_name text,
bio text,
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
@@ -87,6 +89,7 @@ func Make(ctx context.Context, dbPath string) (*DB, error) {
create table if not exists albums (
id integer primary key autoincrement,
title text not null,
+ latin_name text,
release_date text not null,
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
diff --git a/internal/domain/album.go b/internal/domain/album.go
@@ -5,6 +5,7 @@ import "time"
type Album struct {
ID int64
Title string
+ LatinName *string
ReleaseDate time.Time
Artists []AlbumArtist
}
diff --git a/internal/domain/artist.go b/internal/domain/artist.go
@@ -1,7 +1,8 @@
package domain
type Artist struct {
- ID int64
- Name string
- Bio *string
+ ID int64
+ Name string
+ LatinName *string
+ Bio *string
}
diff --git a/internal/domain/song.go b/internal/domain/song.go
@@ -6,10 +6,12 @@ import (
)
var NoTrailingOrLeadingSpaceRegex = regexp.MustCompile(`^\S(\s*\S)*$`)
+var LatinNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z ]*[a-zA-Z]$`)
type Song struct {
ID int64
Title string
+ LatinTitle *string
Lyrics string
ReleaseDate time.Time
Artists []SongArtist
diff --git a/internal/server/new-artist.go b/internal/server/new-artist.go
@@ -10,8 +10,9 @@ import (
)
type NewArtistForm struct {
- Name string `schema:"artist"`
- Bio string `schema:"biography"`
+ Name string `schema:"artist"`
+ Bio string `schema:"biography"`
+ LatinName string `schema:"latin-name"`
}
func parseNewArtistForm(form NewArtistForm) (domain.Artist, error) {
@@ -22,14 +23,20 @@ func parseNewArtistForm(form NewArtistForm) (domain.Artist, error) {
return artist, fmt.Errorf("Artist name must not be empty or have leading/trailing whitespace")
}
+ // latin name
+ if form.LatinName != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.LatinName) {
+ return artist, fmt.Errorf("Latin name must not be empty or have leading/trailing whitespace")
+ }
+
// bio
if form.Bio != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Bio) {
return artist, fmt.Errorf("Artist biography must not be empty or have leading/trailing whitespace")
}
artist = domain.Artist{
- Name: form.Name,
- Bio: &form.Bio,
+ Name: form.Name,
+ LatinName: &form.LatinName,
+ Bio: &form.Bio,
}
return artist, nil
@@ -71,7 +78,7 @@ func (ss *ServerState) NewArtist(w http.ResponseWriter, r *http.Request) {
return
}
- err = db.UpsertArtist(ss.db, newArtist)
+ err = db.InsertArtist(ss.db, newArtist)
if err != nil {
logger.Error("failed to save new artist into db", "err", err)
htmx.HxError(w, http.StatusBadRequest, "Failed to add artist.")
diff --git a/internal/server/new-song.go b/internal/server/new-song.go
@@ -12,6 +12,7 @@ import (
type NewSongForm struct {
Artist []string `schema:"artist"`
Title string `schema:"title"`
+ LatinTitle string `schema:"latin-title"`
Albums []string `schema:"albums"`
Genres []string `schema:"genres"`
Lyrics string `schema:"lyrics"`
@@ -36,6 +37,10 @@ func parseNewSongForm(form NewSongForm) (domain.Song, error) {
return song, fmt.Errorf("Title must not be empty or have leading/trailing whitespace")
}
+ if form.LatinTitle != "" && !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.LatinTitle) {
+ return song, fmt.Errorf("Latin title must not be empty")
+ }
+
if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Lyrics) {
return song, fmt.Errorf("Lyrics must not be empty")
}
@@ -92,6 +97,7 @@ func parseNewSongForm(form NewSongForm) (domain.Song, error) {
Title: form.Title,
Lyrics: form.Lyrics,
ReleaseDate: releaseDate,
+ LatinTitle: &form.LatinTitle,
Genres: genres,
AlbumIDs: albumIDs,
}
diff --git a/internal/server/ui/views/new-artist/new-artist.templ b/internal/server/ui/views/new-artist/new-artist.templ
@@ -13,19 +13,31 @@ templ NewArtist(params NewArtistParams) {
hx-swap="none"
hx-disable-elt="#add-artist-btn"
>
- <label>
- artist:
- <input
- hx-get="/artist/search"
- hx-trigger="input changed delay:500ms, load"
- hx-target="#similar-artists"
- hx-swap="outerHTML"
- name="artist"
- type="text"
- pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
- required
- />
- </label>
+ <div x-data="{ showLatinName: false }">
+ <label>
+ artist:
+ <input
+ hx-get="/artist/search"
+ hx-trigger="input changed delay:500ms, load"
+ hx-target="#similar-artists"
+ hx-swap="outerHTML"
+ name="artist"
+ type="text"
+ pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
+ required
+ @input="showLatinName = /[^\u0000-\u007F]/.test($event.target.value)"
+ />
+ </label>
+ <label x-show="showLatinName">
+ latin name:
+ <input
+ name="latin-name"
+ type="text"
+ :required="showLatinName"
+ pattern={ domain.LatinNameRegex.String() }
+ />
+ </label>
+ </div>
<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
@@ -48,15 +48,27 @@ templ NewSong(params NewSongParams) {
</ol>
<datalist id="artist-suggestions"></datalist>
</fieldset>
- <label>
- title:
- <input
- name="title"
- type="text"
- pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
- required
- />
- </label>
+ <div x-data="{ showLatinName: false }">
+ <label>
+ title:
+ <input
+ name="title"
+ type="text"
+ pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
+ required
+ @input="showLatinName = /[^\u0000-\u007F]/.test($event.target.value)"
+ />
+ </label>
+ <label x-show="showLatinName">
+ latin name:
+ <input
+ name="latin-name"
+ type="text"
+ :required="showLatinName"
+ pattern={ domain.LatinNameRegex.String() }
+ />
+ </label>
+ </div>
<fieldset x-data="{ items: [{ id: 0 }], uid: 1 }">
<legend>
albums