package database import ( "context" "encoding/json" "regexp" "strings" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type DB struct { pool *pgxpool.Pool } func New(ctx context.Context, databaseURL string) (*DB, error) { pool, err := pgxpool.New(ctx, databaseURL) if err != nil { return nil, err } if err := pool.Ping(ctx); err != nil { return nil, err } return &DB{pool: pool}, nil } func (db *DB) Close() { db.pool.Close() } type Artist struct { ID string Name string SortName string ArtistType string Description string Genres []Genre ExternalIDs []ExternalID } type Album struct { ID string Title string AlbumType string ReleaseDate string Genres []Genre } type Genre struct { ID string `json:"id"` Name string `json:"name"` } type ExternalID struct { Source string `json:"source"` SourceID string `json:"source_id"` URL string `json:"url"` } type ArtistMetadataRow struct { ID uuid.UUID `json:"id"` ForeignArtistID *string `json:"foreign_artist_id"` Name string `json:"name"` SortName *string `json:"sort_name"` ArtistType *string `json:"artist_type"` Genres json.RawMessage `json:"genres"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type AlbumRow struct { ID uuid.UUID `json:"id"` ArtistMetadataID uuid.UUID `json:"artist_metadata_id"` ForeignAlbumID *string `json:"foreign_album_id"` Title string `json:"title"` AlbumType *string `json:"album_type"` ReleaseDate *time.Time `json:"release_date"` Monitored bool `json:"monitored"` AddedAt time.Time `json:"added_at"` } type AlbumWithArtistRow struct { ID uuid.UUID `json:"id"` ForeignAlbumID *string `json:"foreign_album_id"` Title string `json:"title"` AlbumType *string `json:"album_type"` ReleaseDate *time.Time `json:"release_date"` Monitored bool `json:"monitored"` AddedAt time.Time `json:"added_at"` ArtistID uuid.UUID `json:"artist_id"` ArtistName string `json:"artist_name"` } func (db *DB) UpsertArtistMetadata(ctx context.Context, artist *Artist) (uuid.UUID, error) { id, err := uuid.Parse(artist.ID) if err != nil { id = uuid.New() } genres, _ := json.Marshal(artist.Genres) links, _ := json.Marshal(artist.ExternalIDs) var resultID uuid.UUID err = db.pool.QueryRow(ctx, ` INSERT INTO artist_metadata ( id, foreign_artist_id, name, sort_name, disambiguation, artist_type, status, overview, genres, links, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) ON CONFLICT (foreign_artist_id) DO UPDATE SET name = EXCLUDED.name, sort_name = EXCLUDED.sort_name, artist_type = EXCLUDED.artist_type, overview = EXCLUDED.overview, genres = EXCLUDED.genres, links = EXCLUDED.links, updated_at = NOW() RETURNING id `, id, artist.ID, artist.Name, artist.SortName, artist.Description, artist.ArtistType, "active", artist.Description, genres, links).Scan(&resultID) return resultID, err } var cleanTitleRegex = regexp.MustCompile(`[^a-z0-9]`) func (db *DB) UpsertAlbum(ctx context.Context, album *Album, artistMetadataID uuid.UUID) (uuid.UUID, error) { id, err := uuid.Parse(album.ID) if err != nil { id = uuid.New() } genres, _ := json.Marshal(album.Genres) images, _ := json.Marshal([]any{}) var releaseDate *time.Time if album.ReleaseDate != "" { if t, err := time.Parse("2006-01-02", album.ReleaseDate); err == nil { releaseDate = &t } } cleanTitle := cleanTitleRegex.ReplaceAllString(strings.ToLower(album.Title), "") var resultID uuid.UUID err = db.pool.QueryRow(ctx, ` INSERT INTO albums ( id, artist_metadata_id, foreign_album_id, title, clean_title, overview, album_type, release_date, images, genres ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (foreign_album_id) DO UPDATE SET title = EXCLUDED.title, album_type = EXCLUDED.album_type, release_date = EXCLUDED.release_date, genres = EXCLUDED.genres RETURNING id `, id, artistMetadataID, album.ID, album.Title, cleanTitle, "", album.AlbumType, releaseDate, images, genres).Scan(&resultID) return resultID, err } func (db *DB) ListArtists(ctx context.Context) ([]ArtistMetadataRow, error) { rows, err := db.pool.Query(ctx, ` SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at FROM artist_metadata ORDER BY name `) if err != nil { return nil, err } defer rows.Close() var artists []ArtistMetadataRow for rows.Next() { var a ArtistMetadataRow err := rows.Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt) if err != nil { return nil, err } artists = append(artists, a) } return artists, nil } func (db *DB) ListAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) ([]AlbumRow, error) { rows, err := db.pool.Query(ctx, ` SELECT id, artist_metadata_id, foreign_album_id, title, album_type, release_date, monitored, added_at FROM albums WHERE artist_metadata_id = $1 ORDER BY release_date DESC NULLS LAST `, artistMetadataID) if err != nil { return nil, err } defer rows.Close() var albums []AlbumRow for rows.Next() { var a AlbumRow err := rows.Scan(&a.ID, &a.ArtistMetadataID, &a.ForeignAlbumID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.Monitored, &a.AddedAt) if err != nil { return nil, err } albums = append(albums, a) } return albums, nil } func (db *DB) ListAllAlbums(ctx context.Context) ([]AlbumWithArtistRow, error) { rows, err := db.pool.Query(ctx, ` SELECT a.id, a.foreign_album_id, a.title, a.album_type, a.release_date, a.monitored, a.added_at, am.id as artist_id, am.name as artist_name FROM albums a JOIN artist_metadata am ON a.artist_metadata_id = am.id ORDER BY a.added_at DESC `) if err != nil { return nil, err } defer rows.Close() var albums []AlbumWithArtistRow for rows.Next() { var a AlbumWithArtistRow err := rows.Scan(&a.ID, &a.ForeignAlbumID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.Monitored, &a.AddedAt, &a.ArtistID, &a.ArtistName) if err != nil { return nil, err } albums = append(albums, a) } return albums, nil } func (db *DB) CountArtists(ctx context.Context) (int64, error) { var count int64 err := db.pool.QueryRow(ctx, "SELECT COUNT(*) FROM artist_metadata").Scan(&count) return count, err } func (db *DB) CountAlbums(ctx context.Context) (int64, error) { var count int64 err := db.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&count) return count, err }