lyrics

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

commit 1df261605d32d2eaacd6b56b0d7d40a8a9a0d08a
parent 89c9fa25ffa7bd3929b60e02c4704b82808fd40c
Author: brookjeynes <me@brookjeynes.dev>
Date:   Sun, 14 Jun 2026 17:48:18 +1000

feat: add MusicBrainz IDs

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

Diffstat:
Minternal/db/album.go | 20+++++++++++---------
Minternal/db/artist.go | 16+++++++++-------
Minternal/db/db.go | 3+++
Minternal/db/song.go | 21+++++++++++++++------
Minternal/domain/album.go | 37++++++++++++++++++++-----------------
Minternal/domain/artist.go | 11++++++-----
Ainternal/domain/musicbrainz.go | 10++++++++++
Minternal/domain/regex.go | 3+++
Minternal/domain/song.go | 34++++++++++++++++++----------------
9 files changed, 95 insertions(+), 60 deletions(-)

diff --git a/internal/db/album.go b/internal/db/album.go @@ -12,10 +12,10 @@ func Album(e Execer, id domain.AlbumID) (domain.Album, error) { var releaseDateStr string err := e.QueryRow(` - select id, title, latin_title, release_date + select id, musicbrainz_release_id, title, latin_title, release_date from albums where id = ? - `, id).Scan(&album.ID, &album.Title, &album.LatinTitle, &releaseDateStr) + `, id).Scan(&album.ID, &album.MusicBrainzReleaseID, &album.Title, &album.LatinTitle, &releaseDateStr) if err != nil { return domain.Album{}, fmt.Errorf("failed to get album: %w", err) } @@ -71,7 +71,7 @@ func Album(e Execer, id domain.AlbumID) (domain.Album, error) { artistMap := make(map[domain.ArtistID]domain.Artist, len(artistIDs)) { rows, err := e.Query(` - select id, name, latin_name, disambiguation, bio + select id, musicbrainz_artist_id, name, latin_name, disambiguation, bio from artists where id in (`+placeholders+`) `, artistIDs...) @@ -81,7 +81,7 @@ func Album(e Execer, id domain.AlbumID) (domain.Album, error) { for rows.Next() { var a domain.Artist - if err := rows.Scan(&a.ID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil { + if err := rows.Scan(&a.ID, &a.MusicBrainzArtistID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil { _ = rows.Close() return domain.Album{}, fmt.Errorf("failed to scan artist: %w", err) } @@ -155,11 +155,12 @@ func UpdateAlbum(e Execer, album domain.EditAlbum) error { result, err := e.Exec(` update albums set + musicbrainz_release_id = ?, title = ?, latin_title = ?, release_date = ? where id = ? - `, album.Title, album.LatinTitle, album.ReleaseDate.Format(time.RFC3339), album.ID) + `, album.MusicBrainzReleaseID, album.Title, album.LatinTitle, album.ReleaseDate.Format(time.RFC3339), album.ID) if err != nil { return fmt.Errorf("failed to update album: %w", err) } @@ -224,13 +225,14 @@ func UpdateAlbum(e Execer, album domain.EditAlbum) error { func InsertAlbum(e Execer, album domain.NewAlbum) error { result, err := e.Exec(` insert into albums ( + musicbrainz_release_id, title, latin_title, release_date, created_at ) - values (?, ?, ?, ?) - `, album.Title, album.LatinTitle, album.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339)) + values (?, ?, ?, ?, ?) + `, album.MusicBrainzReleaseID, album.Title, album.LatinTitle, album.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339)) if err != nil { return fmt.Errorf("failed to insert album: %w", err) } @@ -268,7 +270,7 @@ func SearchAlbum(e Execer, albumName string) ([]domain.Album, error) { wildcard := "%" + albumName + "%" rows, err := e.Query(` - select id, title, latin_title, release_date + select id, musicbrainz_release_id, title, latin_title, release_date from albums where title like ? collate nocase or latin_title like ? collate nocase @@ -287,7 +289,7 @@ func SearchAlbum(e Execer, albumName string) ([]domain.Album, error) { var album domain.Album var releaseDateStr string - if err := rows.Scan(&album.ID, &album.Title, &album.LatinTitle, &releaseDateStr); err != nil { + if err := rows.Scan(&album.ID, &album.MusicBrainzReleaseID, &album.Title, &album.LatinTitle, &releaseDateStr); err != nil { return nil, fmt.Errorf("failed to scan album row: %w", err) } diff --git a/internal/db/artist.go b/internal/db/artist.go @@ -11,10 +11,10 @@ func Artist(e Execer, id domain.ArtistID) (domain.Artist, []domain.Album, []doma var artist domain.Artist err := e.QueryRow(` - select id, name, latin_name, disambiguation, bio + select id, musicbrainz_artist_id, name, latin_name, disambiguation, bio from artists where id = ? - `, id).Scan(&artist.ID, &artist.Name, &artist.LatinName, &artist.Disambiguation, &artist.Bio) + `, id).Scan(&artist.ID, &artist.MusicBrainzArtistID, &artist.Name, &artist.LatinName, &artist.Disambiguation, &artist.Bio) if err != nil { return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to get artist: %w", err) } @@ -142,14 +142,15 @@ func Artist(e Execer, id domain.ArtistID) (domain.Artist, []domain.Album, []doma func InsertArtist(e Execer, artist domain.Artist) error { _, err := e.Exec(` insert into artists ( + musicbrainz_artist_id, name, latin_name, disambiguation, bio, created_at ) - values (?, ?, ?, ?, ?) - `, artist.Name, artist.LatinName, artist.Disambiguation, artist.Bio, time.Now().Format(time.RFC3339)) + values (?, ?, ?, ?, ?, ?) + `, artist.MusicBrainzArtistID, 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) } @@ -161,12 +162,13 @@ func UpdateArtist(e Execer, artist domain.Artist) error { _, err := e.Exec(` update artists set + musicbrainz_artist_id = ?, name = ?, latin_name = ?, disambiguation = ?, bio = ? where id = ? - `, artist.Name, artist.LatinName, artist.Disambiguation, artist.Bio, artist.ID) + `, artist.MusicBrainzArtistID, artist.Name, artist.LatinName, artist.Disambiguation, artist.Bio, artist.ID) if err != nil { return fmt.Errorf("failed to update artist: %w", err) } @@ -182,7 +184,7 @@ func SearchArtist(e Execer, artistName string) ([]domain.Artist, error) { wildcard := "%" + artistName + "%" rows, err := e.Query(` - select id, name, latin_name, disambiguation, bio + select id, musicbrainz_artist_id, name, latin_name, disambiguation, bio from artists where name like ? collate nocase or latin_name like ? collate nocase @@ -199,7 +201,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.Disambiguation, &artist.Bio); err != nil { + if err := rows.Scan(&artist.ID, &artist.MusicBrainzArtistID, &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 @@ -61,6 +61,7 @@ func New(ctx context.Context, dbPath string) (*DB, error) { _, err = tx.ExecContext(ctx, ` create table if not exists songs ( id integer primary key autoincrement, + musicbrainz_recording_id text, title text not null, latin_title text, lyrics text not null, @@ -75,6 +76,7 @@ func New(ctx context.Context, dbPath string) (*DB, error) { _, err = tx.ExecContext(ctx, ` create table if not exists artists ( id integer primary key autoincrement, + musicbrainz_artist_id text, name text not null, latin_name text, disambiguation text, @@ -89,6 +91,7 @@ func New(ctx context.Context, dbPath string) (*DB, error) { _, err = tx.ExecContext(ctx, ` create table if not exists albums ( id integer primary key autoincrement, + musicbrainz_release_id text, title text not null, latin_title text, release_date text not null, diff --git a/internal/db/song.go b/internal/db/song.go @@ -12,10 +12,10 @@ func Song(e Execer, id domain.SongID) (domain.Song, error) { var releaseDateStr string err := e.QueryRow(` - select id, title, latin_title, lyrics, release_date + select id, musicbrainz_recording_id, title, latin_title, lyrics, release_date from songs where id = ? - `, id).Scan(&song.ID, &song.Title, &song.LatinTitle, &song.Lyrics, &releaseDateStr) + `, id).Scan(&song.ID, &song.MusicBrainzRecordingID, &song.Title, &song.LatinTitle, &song.Lyrics, &releaseDateStr) if err != nil { return domain.Song{}, fmt.Errorf("failed to get song: %w", err) } @@ -29,6 +29,7 @@ func Song(e Execer, id domain.SongID) (domain.Song, error) { rows, err := e.Query(` select a.id, + a.musicbrainz_artist_id, a.name, a.latin_name, a.disambiguation, @@ -48,6 +49,7 @@ func Song(e Execer, id domain.SongID) (domain.Song, error) { var artist domain.SongArtist if err := rows.Scan( &artist.Artist.ID, + &artist.Artist.MusicBrainzArtistID, &artist.Artist.Name, &artist.Artist.LatinName, &artist.Artist.Disambiguation, @@ -71,6 +73,7 @@ func Song(e Execer, id domain.SongID) (domain.Song, error) { rows, err := e.Query(` select a.id, + a.musicbrainz_release_id, a.title, a.latin_title, a.release_date, @@ -89,6 +92,7 @@ func Song(e Execer, id domain.SongID) (domain.Song, error) { var albumReleaseDateStr string if err := rows.Scan( &album.Album.ID, + &album.Album.MusicBrainzReleaseID, &album.Album.Title, &album.Album.LatinTitle, &albumReleaseDateStr, @@ -144,7 +148,7 @@ func Song(e Execer, id domain.SongID) (domain.Song, error) { func RecentSongs(e Execer, limit int) ([]domain.Song, error) { rows, err := e.Query(` - select id, title, latin_title, lyrics, release_date + select id, musicbrainz_recording_id, title, latin_title, lyrics, release_date from songs order by created_at desc limit ? @@ -158,7 +162,7 @@ func RecentSongs(e Execer, limit int) ([]domain.Song, error) { var song domain.Song var releaseDateStr string - if err := rows.Scan(&song.ID, &song.Title, &song.LatinTitle, &song.Lyrics, &releaseDateStr); err != nil { + if err := rows.Scan(&song.ID, &song.MusicBrainzRecordingID, &song.Title, &song.LatinTitle, &song.Lyrics, &releaseDateStr); err != nil { _ = rows.Close() return nil, fmt.Errorf("failed to scan recent song: %w", err) } @@ -195,6 +199,7 @@ func RecentSongs(e Execer, limit int) ([]domain.Song, error) { select sa.song_id, a.id, + a.musicbrainz_artist_id, a.name, a.latin_name, a.disambiguation, @@ -216,6 +221,7 @@ func RecentSongs(e Execer, limit int) ([]domain.Song, error) { if err := rows.Scan( &songID, &artist.Artist.ID, + &artist.Artist.MusicBrainzArtistID, &artist.Artist.Name, &artist.Artist.LatinName, &artist.Artist.Disambiguation, @@ -243,6 +249,7 @@ func RecentSongs(e Execer, limit int) ([]domain.Song, error) { select sa.song_id, a.id, + a.musicbrainz_release_id, a.title, a.latin_title, a.release_date, @@ -263,6 +270,7 @@ func RecentSongs(e Execer, limit int) ([]domain.Song, error) { if err := rows.Scan( &songID, &album.Album.ID, + &album.Album.MusicBrainzReleaseID, &album.Album.Title, &album.Album.LatinTitle, &albumReleaseDateStr, @@ -295,14 +303,15 @@ func RecentSongs(e Execer, limit int) ([]domain.Song, error) { func InsertSong(e Execer, song domain.NewSong) error { result, err := e.Exec(` insert into songs ( + musicbrainz_recording_id, title, latin_title, lyrics, release_date, created_at ) - values (?, ?, ?, ?, ?) - `, song.Title, song.LatinTitle, song.Lyrics, song.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339)) + values (?, ?, ?, ?, ?, ?) + `, song.MusicBrainzRecordingID, song.Title, song.LatinTitle, song.Lyrics, song.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339)) if err != nil { return fmt.Errorf("failed to insert song: %w", err) } diff --git a/internal/domain/album.go b/internal/domain/album.go @@ -5,11 +5,12 @@ import "time" type AlbumID int64 type NewAlbum struct { - ID AlbumID - Title string - LatinTitle *string - ReleaseDate time.Time - Artists []NewAlbumArtist + ID AlbumID + MusicBrainzReleaseID *MusicBrainzID + Title string + LatinTitle *string + ReleaseDate time.Time + Artists []NewAlbumArtist } type NewAlbumArtist struct { @@ -19,12 +20,13 @@ type NewAlbumArtist struct { } type EditAlbum struct { - ID AlbumID - Title string - LatinTitle *string - ReleaseDate time.Time - Artists []NewAlbumArtist - Songs []EditAlbumSong + ID AlbumID + MusicBrainzReleaseID *MusicBrainzID + Title string + LatinTitle *string + ReleaseDate time.Time + Artists []NewAlbumArtist + Songs []EditAlbumSong } type EditAlbumSong struct { @@ -33,12 +35,13 @@ type EditAlbumSong struct { } type Album struct { - ID AlbumID - Title string - LatinTitle *string - ReleaseDate time.Time - Artists []AlbumArtist - Songs []AlbumSong + ID AlbumID + MusicBrainzReleaseID *MusicBrainzID + Title string + LatinTitle *string + ReleaseDate time.Time + Artists []AlbumArtist + Songs []AlbumSong } type AlbumArtist struct { diff --git a/internal/domain/artist.go b/internal/domain/artist.go @@ -10,9 +10,10 @@ const ( ) type Artist struct { - ID ArtistID - Name string - LatinName *string - Disambiguation *string - Bio *string + ID ArtistID + MusicBrainzArtistID *MusicBrainzID + Name string + LatinName *string + Disambiguation *string + Bio *string } diff --git a/internal/domain/musicbrainz.go b/internal/domain/musicbrainz.go @@ -0,0 +1,10 @@ +package domain + +type MusicBrainzID string + +func MusicBrainzIDIsValid(id string) bool { + if !IsValidMusicBrainzIDRegex.MatchString(id) { + return false + } + return true +} diff --git a/internal/domain/regex.go b/internal/domain/regex.go @@ -5,13 +5,16 @@ import "regexp" const ( noTrailingOrLeadingSpacePattern = `\S(.*\S)?` validSortNameCharsPattern = `[a-zA-ZÀ-ÖØ-öø-ÿ0-9 ',.\-&!\(\)]` + musicBrainzIDPattern = `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}` ) // Go regexes (server-side validation) var ( NoTrailingOrLeadingSpaceRegex = regexp.MustCompile(`^` + noTrailingOrLeadingSpacePattern + `$`) IsValidSortNameRegex = regexp.MustCompile(`^` + validSortNameCharsPattern + `+$`) + IsValidMusicBrainzIDRegex = regexp.MustCompile(`^` + musicBrainzIDPattern + `$`) ) // HTML pattern (client-side validation with lookahead) const IsValidSortNamePattern = `(?=^` + noTrailingOrLeadingSpacePattern + `$)` + validSortNameCharsPattern + `+$` +const IsValidMusicBrainzIDPattern = musicBrainzIDPattern diff --git a/internal/domain/song.go b/internal/domain/song.go @@ -7,14 +7,15 @@ import ( type SongID int64 type NewSong struct { - ID SongID - Title string - LatinTitle *string - Lyrics string - ReleaseDate time.Time - Artists []NewSongArtist - Albums []NewSongAlbum - Genres []Genre + ID SongID + MusicBrainzRecordingID *MusicBrainzID + Title string + LatinTitle *string + Lyrics string + ReleaseDate time.Time + Artists []NewSongArtist + Albums []NewSongAlbum + Genres []Genre } type NewSongArtist struct { @@ -29,14 +30,15 @@ type NewSongAlbum struct { } type Song struct { - ID SongID - Title string - LatinTitle *string - Lyrics string - ReleaseDate time.Time - Artists []SongArtist - Albums []SongAlbum - Genres []Genre + ID SongID + MusicBrainzRecordingID *MusicBrainzID + Title string + LatinTitle *string + Lyrics string + ReleaseDate time.Time + Artists []SongArtist + Albums []SongAlbum + Genres []Genre } type SongArtist struct {