badger/internal/metadata/metadata.go
2023-03-14 09:53:33 +01:00

256 lines
7.2 KiB
Go

package metadata
import (
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"velvettear/badger/internal/config"
"velvettear/badger/internal/database"
"velvettear/badger/internal/database/models"
"velvettear/badger/internal/executables"
"velvettear/badger/internal/log"
"velvettear/badger/internal/tools"
"gorm.io/gorm"
)
// exported function(s)
func FromFile(file string) Metadata {
timestamp := tools.LogTimestamp()
metadata := Metadata{Path: file}
executable, error := executables.GetExecutable(executables.FFPROBE)
if error != nil {
log.Fatal("could not get executable '"+executables.FFPROBE+"'", error.Error())
}
result, error := executable.Spawn("-show_format", "-show_streams", "-loglevel", "quiet", "-print_format", "flat", file)
keys := getKeys()
resultElements := strings.Split(result, "\n")
for elementIndex := 0; elementIndex < len(resultElements); elementIndex++ {
element := strings.Trim(resultElements[elementIndex], " ")
if !strings.HasPrefix(strings.ToLower(element), METADATA_PREFIX) {
continue
}
for keyIndex := 0; keyIndex < len(keys); keyIndex++ {
key := keys[keyIndex]
cut := strings.Index(element, "=")
if cut == -1 {
continue
}
elementKey := strings.Trim(strings.ToLower(element[0:cut]), " ")
elementKey = elementKey[strings.LastIndex(elementKey, ".")+1:]
if key != elementKey {
continue
}
element = strings.Trim(strings.Trim(element[cut+1:], "\""), " ")
switch key {
case METADATA_TITLE:
metadata.Title = element
case METADATA_ALBUM:
metadata.Album = element
case METADATA_ARTIST:
metadata.Artist = element
case METADATA_ALBUM_ARTIST:
separators := []string{",", ";", "|"}
for index := 0; index < len(separators); index++ {
element = strings.ReplaceAll(element, separators[index], "/")
}
parts := strings.Split(element, "/")
for index := 0; index < len(parts); index++ {
metadata.AlbumArtist = append(metadata.AlbumArtist, strings.Trim(parts[index], " "))
}
case METADATA_DATE:
metadata.Date = element
case METADATA_TRACK:
metadata.TrackNo = strings.Split(element, "/")[0]
case METADATA_DISC:
metadata.DiscNo = strings.Split(element, "/")[0]
case METADATA_DURATION:
metadata.Duration = element
case METADATA_BITRATE:
metadata.Bitrate, _ = strconv.Atoi(element)
}
}
}
stats, error := os.Stat(metadata.Path)
if error != nil {
log.Error("encountered an error getting the stats for file '"+metadata.Path+"'", error.Error())
}
metadata.Modified = stats.ModTime()
metadata.Size = stats.Size()
metadata.generateFingerprint()
metadata.generateChecksum()
log.DebugTimed("parsed file '"+metadata.Path+"'", timestamp)
metadata.Complete = true
return metadata
}
func (metadata *Metadata) GenerateTargetPath() string {
library := config.LibraryDirectory()
artist := strings.Join(metadata.AlbumArtist, " - ")
artist = strings.TrimRight(artist, " - ")
if tools.IsEmpty(artist) {
if tools.IsEmpty(metadata.Artist) {
artist = "unknown"
} else {
artist = metadata.Artist
}
}
library += string(os.PathSeparator) + artist
if tools.NotEmpty(metadata.Album) {
library += string(os.PathSeparator) + metadata.Album
}
return filepath.Clean(library)
}
func (metadata *Metadata) GenerateFileName() string {
var filename string
if tools.NotEmpty(metadata.DiscNo) {
filename += tools.FrontFill(metadata.DiscNo, "0", 2)
}
if tools.NotEmpty(metadata.TrackNo) {
if !tools.IsEmpty(filename) {
filename += "-"
}
filename += tools.FrontFill(metadata.TrackNo, "0", 2)
}
if tools.NotEmpty(metadata.Artist) {
if tools.NotEmpty(filename) {
filename += " "
}
filename += metadata.Artist
}
if tools.NotEmpty(metadata.Title) {
if tools.NotEmpty(filename) {
filename += " - "
}
filename += metadata.Title
}
if tools.IsEmpty(filename) {
return ""
}
return filename + filepath.Ext(metadata.Path)
}
// func (metadata *Metadata) ShouldBeMoved(target string) bool {
// if len(target) == 0 {
// return false
// }
// existing := tools.Exists(target)
// var track models.Track
// database.Connection().First(&track, models.Track{Path: target})
// if track.ID == 0 && !existing {
// log.Debug("file '" + metadata.Path + "' should be moved to '" + target + "', no entry in database and target does not exist")
// return true
// }
// if metadata.Bitrate < track.Bitrate {
// log.Debug("file '" + metadata.Path + "' should be moved to '" + target + "', no entry in database and target does not exist")
// return false
// }
// if existing {
// error := tools.DeleteFile(track.Path)
// if error != nil {
// log.Error("encountered an error deleting the file '" + track.Path + "' in order to replace it with '" + metadata.Path + "' ")
// return false
// }
// }
// return true
// }
func (metadata *Metadata) Store() error {
timestamp := tools.LogTimestamp()
if !metadata.Complete {
return errors.New("can not store incomplete metadata")
}
artist := models.Artist{
Name: metadata.Artist,
}
var connection *gorm.DB
if config.DatabaseInMemory() {
connection = database.Memory()
} else {
connection = database.File()
}
connection.FirstOrCreate(&artist, artist)
if artist.ID <= 0 {
return errors.New("could not find or create artist '" + artist.Name + "' in database")
}
album := models.Album{
Name: metadata.Album,
}
connection.FirstOrCreate(&album, album)
if album.ID <= 0 {
return errors.New("could not find or create album '" + album.Name + "' in database")
}
track := models.Track{
Path: metadata.Path,
Title: metadata.Title,
Date: metadata.Title,
TrackNo: metadata.TrackNo,
DiscNo: metadata.DiscNo,
Duration: metadata.Duration,
Bitrate: metadata.Bitrate,
Fingerprint: metadata.Fingerprint.RawValue,
Checksum: metadata.Checksum,
Modified: metadata.Modified.UnixMilli(),
Size: metadata.Size,
ArtistID: artist.ID,
AlbumID: album.ID,
}
connection.FirstOrCreate(&track, models.Track{Path: track.Path})
if album.ID <= 0 {
return errors.New("could not find or create track '" + track.Path + "' in database")
}
log.DebugTimed("stored parsed metadata for file '"+metadata.Path+"' in database", timestamp)
return nil
}
// unexported function(s)
func getKeys() []string {
return []string{
METADATA_TITLE,
METADATA_ALBUM,
METADATA_ARTIST,
METADATA_ALBUM_ARTIST,
METADATA_DATE,
METADATA_TRACK,
METADATA_DISC,
METADATA_DURATION,
METADATA_BITRATE,
}
}
func (metadata *Metadata) generateChecksum() {
if !config.ChangeDetectionChecksum() {
return
}
timestamp := tools.LogTimestamp()
checksum, error := tools.XxHash(metadata.Path)
if error != nil {
log.Error("encountered an error generating xxhash checksum for file '"+metadata.Path+"'", error.Error())
return
}
log.DebugTimed("generated xxhash checksum for file '"+metadata.Path+"'", timestamp)
metadata.Checksum = strconv.FormatUint(checksum, 10)
}
// struct(s)
type Metadata struct {
Title string
Album string
Artist string
AlbumArtist []string
Date string
TrackNo string
DiscNo string
Duration string
Bitrate int
Path string
Fingerprint Fingerprint
Modified time.Time
Size int64
Checksum string
Complete bool
}