lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

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:
Minternal/db/artist.go | 44++++++++++++++++++++++++++++++--------------
Minternal/db/db.go | 3+++
Minternal/domain/album.go | 1+
Minternal/domain/artist.go | 7++++---
Minternal/domain/song.go | 2++
Minternal/server/new-artist.go | 17++++++++++++-----
Minternal/server/new-song.go | 6++++++
Minternal/server/ui/views/new-artist/new-artist.templ | 38+++++++++++++++++++++++++-------------
Minternal/server/ui/views/new-song/new-song.templ | 30+++++++++++++++++++++---------
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