257 lines
7.2 KiB
Go
257 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
|
||
|
}
|