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 }