Library indexing and filesystem scanner with incremental rescans
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
// 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}
|
||||
}
|
||||
Reference in New Issue
Block a user