Files
vibesonic/internal/library/index.go
T

172 lines
5.2 KiB
Go

// Package library turns parsed file metadata into the relational tag
// model (artists, albums, songs, genres, credits) that both filesystem-
// style and ID3-style Subsonic browsing will read from in later phases.
package library
import (
"database/sql"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
"git.noroutes.net/NoRoutes/vibetunes/internal/metadata"
)
// IndexFile creates or updates the songs row (and its album/artist/genre
// relations) for a scanned media file, given its parsed tags. It is
// idempotent: rerunning it for the same file converges to the same rows,
// reusing existing artist/album/genre IDs via their natural keys so IDs
// stay stable across rescans.
func IndexFile(db *sql.DB, mediaFile *database.MediaFile, tags *metadata.Tags) (*database.Song, error) {
title := tags.Title
if title == "" {
title = mediaFile.RelativePath
}
primaryArtistID, err := resolvePrimaryAlbumArtist(db, tags)
if err != nil {
return nil, err
}
var albumID sql.NullString
if tags.Album != "" {
album, err := database.FindOrCreateAlbum(db, tags.Album, primaryArtistID, tags.SortAlbum,
intToNull(tags.Year), intToNull(tags.OriginalYear), tags.RecordLabel, tags.MusicBrainzAlbumID)
if err != nil {
return nil, err
}
albumID = sql.NullString{String: album.ID, Valid: true}
if err := linkAlbumArtists(db, album.ID, tags, primaryArtistID); err != nil {
return nil, err
}
for _, name := range tags.Genres {
genre, err := database.FindOrCreateGenre(db, name)
if err != nil {
return nil, err
}
if err := database.LinkAlbumGenre(db, album.ID, genre.ID); err != nil {
return nil, err
}
}
}
song, err := database.UpsertSong(db, database.UpsertSongInput{
MediaFileID: mediaFile.ID,
AlbumID: albumID,
Title: title,
SortTitle: tags.SortTitle,
TrackNumber: intToNull(tags.TrackNumber),
TrackTotal: intToNull(tags.TrackTotal),
DiscNumber: intToNull(tags.DiscNumber),
DiscTotal: intToNull(tags.DiscTotal),
DiscSubtitle: tags.DiscSubtitle,
Composer: tags.Composer,
Conductor: tags.Conductor,
Comment: tags.Comment,
ISRC: tags.ISRC,
BPM: intToNull(tags.BPM),
MusicBrainzTrackID: tags.MusicBrainzTrackID,
MusicBrainzRecordingID: tags.MusicBrainzRecordingID,
ReplayGainTrackGainDB: floatPtrToNull(tags.ReplayGainTrackGainDB),
ReplayGainTrackPeak: floatPtrToNull(tags.ReplayGainTrackPeak),
ReplayGainAlbumGainDB: floatPtrToNull(tags.ReplayGainAlbumGainDB),
ReplayGainAlbumPeak: floatPtrToNull(tags.ReplayGainAlbumPeak),
})
if err != nil {
return nil, err
}
if err := database.ClearSongArtists(db, song.ID); err != nil {
return nil, err
}
for i, name := range tags.Artists {
artist, err := database.FindOrCreateArtist(db, name, "", "")
if err != nil {
return nil, err
}
if err := database.LinkSongArtist(db, song.ID, artist.ID, "artist", i); err != nil {
return nil, err
}
}
if err := database.ClearContributors(db, song.ID); err != nil {
return nil, err
}
for i, c := range tags.Contributors {
artist, err := database.FindOrCreateArtist(db, c.Name, "", "")
if err != nil {
return nil, err
}
if err := database.LinkContributor(db, song.ID, artist.ID, c.Role, i); err != nil {
return nil, err
}
}
if err := database.ClearSongGenres(db, song.ID); err != nil {
return nil, err
}
for _, name := range tags.Genres {
genre, err := database.FindOrCreateGenre(db, name)
if err != nil {
return nil, err
}
if err := database.LinkSongGenre(db, song.ID, genre.ID); err != nil {
return nil, err
}
}
return song, nil
}
// resolvePrimaryAlbumArtist picks the artist an album groups under:
// the explicit album artist tag if present, otherwise the first
// performer credited on the track.
func resolvePrimaryAlbumArtist(db *sql.DB, tags *metadata.Tags) (sql.NullString, error) {
name := tags.AlbumArtist
sortName := tags.SortAlbumArtist
if name == "" && len(tags.Artists) > 0 {
name = tags.Artists[0]
}
if name == "" {
return sql.NullString{}, nil
}
artist, err := database.FindOrCreateArtist(db, name, sortName, "")
if err != nil {
return sql.NullString{}, err
}
return sql.NullString{String: artist.ID, Valid: true}, nil
}
func linkAlbumArtists(db *sql.DB, albumID string, tags *metadata.Tags, primaryArtistID sql.NullString) error {
if tags.AlbumArtist != "" {
if primaryArtistID.Valid {
return database.LinkAlbumArtist(db, albumID, primaryArtistID.String, 0)
}
return nil
}
// No explicit album artist: treat every track performer as an album artist.
for i, name := range tags.Artists {
artist, err := database.FindOrCreateArtist(db, name, "", "")
if err != nil {
return err
}
if err := database.LinkAlbumArtist(db, albumID, artist.ID, i); err != nil {
return err
}
}
return nil
}
func intToNull(n int) sql.NullInt64 {
if n == 0 {
return sql.NullInt64{}
}
return sql.NullInt64{Int64: int64(n), Valid: true}
}
func floatPtrToNull(f *float64) sql.NullFloat64 {
if f == nil {
return sql.NullFloat64{}
}
return sql.NullFloat64{Float64: *f, Valid: true}
}