154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
package metadata
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// ExtractionTiming holds sub-operation durations from a single
|
|
// ExtractAllMetadata call so callers can build per-format aggregates.
|
|
type ExtractionTiming struct {
|
|
TagExtraction time.Duration
|
|
DurationExtraction time.Duration
|
|
}
|
|
|
|
// AudioProperties holds technical properties of an audio file that
|
|
// are extracted from its stream headers during scanning.
|
|
type AudioProperties struct {
|
|
SampleRate int // Sample rate in Hz (e.g. 44100, 96000).
|
|
BitDepth int // Bits per sample (e.g. 16, 24).
|
|
Channels int // Number of audio channels (1=mono, 2=stereo).
|
|
Bitrate int // Bitrate in kbps.
|
|
FileSize int64 // File size in bytes.
|
|
}
|
|
|
|
// AudioFileExtension represents a supported audio file extension.
|
|
type AudioFileExtension string
|
|
|
|
// Supported audio file extensions.
|
|
const (
|
|
MP3 AudioFileExtension = ".mp3"
|
|
FLAC AudioFileExtension = ".flac"
|
|
OGG AudioFileExtension = ".ogg"
|
|
WAV AudioFileExtension = ".wav"
|
|
)
|
|
|
|
// SupportedFileExtensions lists all supported audio formats.
|
|
var SupportedFileExtensions = []AudioFileExtension{MP3, FLAC, OGG, WAV}
|
|
|
|
// GetSupportedFileType checks if a file extension is supported.
|
|
func GetSupportedFileType(ext string) (AudioFileExtension, bool) {
|
|
for _, supported := range SupportedFileExtensions {
|
|
if string(supported) == ext {
|
|
return supported, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// GetTrackLengthMillis returns the duration of an audio file in milliseconds.
|
|
func GetTrackLengthMillis(path string) (int64, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("could not open file: %w", err)
|
|
}
|
|
|
|
streamer, format, err := DecodeFile(f)
|
|
if err != nil {
|
|
_ = f.Close()
|
|
|
|
return 0, fmt.Errorf("error decoding file: %w", err)
|
|
}
|
|
|
|
lengthMillis := int64(float64(streamer.Len()*1000) / float64(format.SampleRate))
|
|
_ = streamer.Close()
|
|
_ = f.Close()
|
|
|
|
return lengthMillis, nil
|
|
}
|
|
|
|
// ExtractAllMetadata opens the file once and extracts tags, duration,
|
|
// and audio properties (sample rate, bit depth, channels, bitrate,
|
|
// file size). If skipDuration is true, only tags are extracted and
|
|
// the remaining outputs are zero-valued.
|
|
// The returned ExtractionTiming records how long each sub-operation took.
|
|
func ExtractAllMetadata(
|
|
path string,
|
|
skipDuration bool,
|
|
) (*TrackMetadata, int64, *AudioProperties, *ExtractionTiming, error) {
|
|
timing := &ExtractionTiming{}
|
|
props := &AudioProperties{}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, 0, props, timing, fmt.Errorf(
|
|
"could not open file: %w", err,
|
|
)
|
|
}
|
|
|
|
defer func() { _ = f.Close() }()
|
|
|
|
// Capture file size.
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return nil, 0, props, timing, fmt.Errorf(
|
|
"could not stat file: %w", err,
|
|
)
|
|
}
|
|
|
|
props.FileSize = fi.Size()
|
|
|
|
// Extract tags first (reads only headers, fast).
|
|
tagStart := time.Now()
|
|
|
|
tags, err := ExtractTagsFromReader(f)
|
|
|
|
timing.TagExtraction = time.Since(tagStart)
|
|
|
|
if err != nil {
|
|
return nil, 0, props, timing, fmt.Errorf(
|
|
"could not extract tags from %s: %w", path, err,
|
|
)
|
|
}
|
|
|
|
if skipDuration {
|
|
return tags, 0, props, timing, nil
|
|
}
|
|
|
|
// Seek back to the beginning for duration extraction.
|
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
|
return tags, 0, props, timing, fmt.Errorf(
|
|
"could not seek file for duration: %w", err,
|
|
)
|
|
}
|
|
|
|
durStart := time.Now()
|
|
|
|
lengthMillis, audioProps, err := getTrackDuration(f)
|
|
|
|
timing.DurationExtraction = time.Since(durStart)
|
|
|
|
if err != nil {
|
|
return tags, 0, props, timing, fmt.Errorf(
|
|
"error getting duration for %s: %w", path, err,
|
|
)
|
|
}
|
|
|
|
// Merge stream properties into the result, keeping the
|
|
// file size we already captured.
|
|
audioProps.FileSize = props.FileSize
|
|
|
|
// Compute bitrate from file size and duration when the
|
|
// format parser did not provide one (lossless formats).
|
|
if audioProps.Bitrate == 0 && lengthMillis > 0 {
|
|
audioProps.Bitrate = int(
|
|
props.FileSize * 8 / lengthMillis,
|
|
)
|
|
}
|
|
|
|
return tags, lengthMillis, audioProps, timing, nil
|
|
}
|