176 lines
5.3 KiB
Go
176 lines
5.3 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
|
|
}
|
|
}
|
|
|
|
if err := database.SetSongLyrics(db, song.ID, "xxx", tags.EmbeddedLyrics); 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}
|
|
}
|