lyrics

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

commit 2f27b711f4c63c65133c76519d09cbd38002ca6b
parent e5f4ff39a63baf26b1c2418fae2eab9871378a87
Author: brookjeynes <me@brookjeynes.dev>
Date:   Wed, 17 Jun 2026 08:57:17 +1000

refactor: add release_type

Task-id: tqszpqrlzkvqrqkz
Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Minternal/db/db.go | 2++
Minternal/db/release.go | 16+++++++++-------
Minternal/domain/release.go | 19+++++++++++++++++++
Minternal/server/edit-release.go | 12+++++++++++-
Minternal/server/new-release.go | 12+++++++++++-
Minternal/server/search-release.go | 2+-
Ainternal/server/ui/views/artist/artist.css | 5+++++
Minternal/server/ui/views/artist/artist.templ | 16++++++++++------
Minternal/server/ui/views/edit-release/edit-release.templ | 34++++++++++++++++++++++++++++++++--
Minternal/server/ui/views/new-release/new-release.templ | 14++++++++++++--
Minternal/server/ui/views/new-song/new-song.templ | 2+-
Minternal/server/ui/views/release/release.templ | 4++++
Mscripts/build-assets.sh | 1+
Mtasks/add_release_types.txt | 2+-
14 files changed, 119 insertions(+), 22 deletions(-)

diff --git a/internal/db/db.go b/internal/db/db.go @@ -95,6 +95,8 @@ func New(ctx context.Context, dbPath string) (*DB, error) { title text not null, latin_title text, release_date text not null, + -- album, single, ep, other + type text not null, created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), unique(title) diff --git a/internal/db/release.go b/internal/db/release.go @@ -12,10 +12,10 @@ func Release(e Execer, id domain.ReleaseID) (domain.Release, error) { var releaseDateStr string err := e.QueryRow(` - select id, musicbrainz_release_id, title, latin_title, release_date + select id, musicbrainz_release_id, title, latin_title, release_date, type from releases where id = ? - `, id).Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr) + `, id).Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr, &release.Type) if err != nil { return domain.Release{}, fmt.Errorf("failed to get release: %w", err) } @@ -155,12 +155,13 @@ func UpdateRelease(e Execer, release domain.EditRelease) error { result, err := e.Exec(` update releases set + type = ?, musicbrainz_release_id = ?, title = ?, latin_title = ?, release_date = ? where id = ? - `, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), release.ID) + `, release.Type, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), release.ID) if err != nil { return fmt.Errorf("failed to update release: %w", err) } @@ -225,14 +226,15 @@ func UpdateRelease(e Execer, release domain.EditRelease) error { func InsertRelease(e Execer, release domain.NewRelease) error { result, err := e.Exec(` insert into releases ( + type, musicbrainz_release_id, title, latin_title, release_date, created_at ) - values (?, ?, ?, ?, ?) - `, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339)) + values (?, ?, ?, ?, ?, ?) + `, release.Type, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339)) if err != nil { return fmt.Errorf("failed to insert release: %w", err) } @@ -270,7 +272,7 @@ func SearchRelease(e Execer, releaseName string) ([]domain.Release, error) { wildcard := "%" + releaseName + "%" rows, err := e.Query(` - select id, musicbrainz_release_id, title, latin_title, release_date + select id, musicbrainz_release_id, title, latin_title, release_date, type from releases where title like ? collate nocase or latin_title like ? collate nocase @@ -289,7 +291,7 @@ func SearchRelease(e Execer, releaseName string) ([]domain.Release, error) { var release domain.Release var releaseDateStr string - if err := rows.Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr); err != nil { + if err := rows.Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr, &release.Type); err != nil { return nil, fmt.Errorf("failed to scan release row: %w", err) } diff --git a/internal/domain/release.go b/internal/domain/release.go @@ -2,6 +2,22 @@ package domain import "time" +type ReleaseType string + +const ( + ReleaseTypeAlbum ReleaseType = "album" + ReleaseTypeSingle ReleaseType = "single" + ReleaseTypeEP ReleaseType = "ep" + ReleaseTypeOther ReleaseType = "other" +) + +var ReleaseTypes map[ReleaseType]struct{} = map[ReleaseType]struct{}{ + ReleaseTypeAlbum: {}, + ReleaseTypeSingle: {}, + ReleaseTypeEP: {}, + ReleaseTypeOther: {}, +} + type ReleaseID int64 type NewRelease struct { @@ -11,6 +27,7 @@ type NewRelease struct { LatinTitle *string ReleaseDate time.Time Artists []NewReleaseArtist + Type ReleaseType } type NewReleaseArtist struct { @@ -27,6 +44,7 @@ type EditRelease struct { ReleaseDate time.Time Artists []NewReleaseArtist Songs []EditReleaseSong + Type ReleaseType } type EditReleaseSong struct { @@ -42,6 +60,7 @@ type Release struct { ReleaseDate time.Time Artists []ReleaseArtist Songs []ReleaseSong + Type ReleaseType } type ReleaseArtist struct { diff --git a/internal/server/edit-release.go b/internal/server/edit-release.go @@ -14,7 +14,8 @@ import ( ) type EditReleaseForm struct { - Title string `schema:"release"` + Title string `schema:"title"` + Type domain.ReleaseType `schema:"type"` LatinTitle string `schema:"latin-title"` ArtistID []domain.ArtistID `schema:"artist-id"` SongID []domain.SongID `schema:"song-id"` @@ -25,6 +26,14 @@ type EditReleaseForm struct { func parseEditReleaseForm(form EditReleaseForm) (domain.EditRelease, error) { var release domain.EditRelease + // type + if form.Type == "" { + return release, fmt.Errorf("Type must not be empty") + } + if _, ok := domain.ReleaseTypes[form.Type]; !ok { + return release, fmt.Errorf("Invalid type selected") + } + // title if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) { return release, fmt.Errorf("Title name must not be empty or have leading/trailing whitespace") @@ -112,6 +121,7 @@ func parseEditReleaseForm(form EditReleaseForm) (domain.EditRelease, error) { ReleaseDate: releaseDate, Artists: artists, Songs: songs, + Type: form.Type, } return release, nil diff --git a/internal/server/new-release.go b/internal/server/new-release.go @@ -13,7 +13,8 @@ import ( ) type NewReleaseForm struct { - Title string `schema:"release"` + Title string `schema:"title"` + Type domain.ReleaseType `schema:"type"` LatinTitle string `schema:"latin-title"` ArtistID []domain.ArtistID `schema:"artist-id"` ReleaseDate string `schema:"release-date"` @@ -23,6 +24,14 @@ type NewReleaseForm struct { func parseNewReleaseForm(form NewReleaseForm) (domain.NewRelease, error) { var release domain.NewRelease + // type + if form.Type == "" { + return release, fmt.Errorf("Type must not be empty") + } + if _, ok := domain.ReleaseTypes[form.Type]; !ok { + return release, fmt.Errorf("Invalid type selected") + } + // title if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) { return release, fmt.Errorf("Title name must not be empty or have leading/trailing whitespace") @@ -91,6 +100,7 @@ func parseNewReleaseForm(form NewReleaseForm) (domain.NewRelease, error) { LatinTitle: latinTitle, ReleaseDate: releaseDate, Artists: artists, + Type: form.Type, } return release, nil diff --git a/internal/server/search-release.go b/internal/server/search-release.go @@ -27,7 +27,7 @@ func (ss *ServerState) SearchRelease(w http.ResponseWriter, r *http.Request) { return } - release := r.URL.Query().Get("release") + release := r.URL.Query().Get("title") results, err := db.SearchRelease(ss.db, release) if err != nil { logger.Error("failed to query db for releases", "err", err) diff --git a/internal/server/ui/views/artist/artist.css b/internal/server/ui/views/artist/artist.css @@ -0,0 +1,5 @@ +.release-title { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/internal/server/ui/views/artist/artist.templ b/internal/server/ui/views/artist/artist.templ @@ -7,7 +7,7 @@ import ( ) templ Artist(params ArtistParams) { - @base.Base(base.BaseParams{Title: params.Artist.Name, PageStylesheets: []string{}}) { + @base.Base(base.BaseParams{Title: params.Artist.Name, PageStylesheets: []string{"artist.css"}}) { <article> <header> <hgroup> @@ -40,9 +40,13 @@ templ Artist(params ArtistParams) { } </a> </h3> - <time aria-label="Release date" datetime={ release.ReleaseDate.Format(time.DateOnly) }> - { release.ReleaseDate.Format("Jan 2, 2006") } - </time> + <div> + <span>{ release.Type }</span> + ยท + <time aria-label="Release date" datetime={ release.ReleaseDate.Format(time.DateOnly) }> + { release.ReleaseDate.Format("Jan 2, 2006") } + </time> + </div> </header> <section aria-label="Track List"> <ol> @@ -63,8 +67,8 @@ templ Artist(params ArtistParams) { } </ol> </section> - <section aria-label="Singles"> - <h2>Singles</h2> + <section aria-label="Songs"> + <h2>Songs</h2> <ul> for _, song := range params.Singles { <li> diff --git a/internal/server/ui/views/edit-release/edit-release.templ b/internal/server/ui/views/edit-release/edit-release.templ @@ -17,16 +17,46 @@ templ EditRelease(params EditReleaseParams) { hx-swap="none" hx-disable-elt="#save-release-btn" > + <fieldset> + <legend>Type</legend> + <label for="type">type:</label> + <select name="type" required> + <option + if params.Release.Type == "album" { + selected + } + value="album" + >album</option> + <option + if params.Release.Type == "single" { + selected + } + value="single" + >single</option> + <option + if params.Release.Type == "ep" { + selected + } + value="ep" + >ep</option> + <option + if params.Release.Type == "other" { + selected + } + value="other" + >other</option> + </select> + </fieldset> <fieldset x-data="{ showLatinTitle: false }" x-init="showLatinTitle = isNonAscii($refs.releaseTitle.value)" > - <legend>Release</legend> + <legend>Title</legend> <label> title: <input x-ref="releaseTitle" - name="release" + name="title" type="text" value={ params.Release.Title } pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() } diff --git a/internal/server/ui/views/new-release/new-release.templ b/internal/server/ui/views/new-release/new-release.templ @@ -16,8 +16,18 @@ templ NewRelease(params NewReleaseParams) { hx-swap="none" hx-disable-elt="#add-release-btn" > + <fieldset> + <legend>Type</legend> + <label for="type">type:</label> + <select name="type" required> + <option value="album">album</option> + <option value="single">single</option> + <option value="ep">ep</option> + <option value="other">other</option> + </select> + </fieldset> <fieldset x-data="{ showLatinTitle: false }"> - <legend>Release</legend> + <legend>Title</legend> <label> title: <input @@ -25,7 +35,7 @@ templ NewRelease(params NewReleaseParams) { hx-trigger="input changed delay:500ms" hx-target="#similar-releases" hx-swap="innerHTML" - name="release" + name="title" type="text" pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() } required diff --git a/internal/server/ui/views/new-song/new-song.templ b/internal/server/ui/views/new-song/new-song.templ @@ -56,7 +56,7 @@ templ NewSong(params NewSongParams) { hx-trigger="input changed delay:500ms" hx-target="next .picker-options" hx-swap="outerHTML" - name="release" + name="title" autocomplete="off" :required="!selected && id > 0" /> diff --git a/internal/server/ui/views/release/release.templ b/internal/server/ui/views/release/release.templ @@ -63,6 +63,10 @@ templ Release(params ReleaseParams) { { params.Release.ReleaseDate.Format("Jan 2, 2006") } </time> </dd> + <dt>Type</dt> + <dd> + <span>{ params.Release.Type }</span> + </dd> if params.Release.MusicBrainzReleaseID != nil { <dt>MusicBrainz release ID</dt> <dd> diff --git a/scripts/build-assets.sh b/scripts/build-assets.sh @@ -15,6 +15,7 @@ cp static/vendor/*.min.js static/files/js/ minify internal/server/ui/style.css -o static/files/style.css minify internal/server/toast/toast.css -o static/files/toast.css minify internal/server/ui/views/song/song.css -o static/files/song.css +minify internal/server/ui/views/artist/artist.css -o static/files/artist.css minify internal/server/ui/views/release/release.css -o static/files/release.css minify internal/server/ui/views/new-release/new-release.css -o static/files/new-release.css minify internal/server/ui/views/new-song/new-song.css -o static/files/new-song.css diff --git a/tasks/add_release_types.txt b/tasks/add_release_types.txt @@ -2,7 +2,7 @@ id: tqszpqrlzkvqrqkz [x] "album" is renamed to "release" -[ ] follow musicbrainz modal of "album", "single", "ep", "other" +[x] follow musicbrainz modal of "album", "single", "ep", "other" [ ] songs must have an explicit release attached to them. [ ] new song page has a "create as single" which will - create a song