Squash merge audio-player-component into main
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// Package metadata handles audio file decoding and metadata extraction.
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/TheCodeOfCaleb/beep/v2"
|
||||
"github.com/TheCodeOfCaleb/beep/v2/flac"
|
||||
"github.com/TheCodeOfCaleb/beep/v2/mp3"
|
||||
"github.com/TheCodeOfCaleb/beep/v2/vorbis"
|
||||
"github.com/TheCodeOfCaleb/beep/v2/wav"
|
||||
)
|
||||
|
||||
// ErrUnsupportedFileType is returned when the audio file type is not supported.
|
||||
var ErrUnsupportedFileType = errors.New("unsupported file type")
|
||||
|
||||
// DecodeFile decodes an audio file into a stream seeker and format.
|
||||
func DecodeFile(f *os.File) (beep.StreamSeekCloser, beep.Format, error) {
|
||||
ext := filepath.Ext(f.Name())
|
||||
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return mp3.Decode(f)
|
||||
case ".flac":
|
||||
return flac.Decode(f)
|
||||
case ".ogg":
|
||||
return vorbis.Decode(f)
|
||||
case ".wav":
|
||||
return wav.Decode(f)
|
||||
default:
|
||||
return nil, beep.Format{}, fmt.Errorf("%w: %s", ErrUnsupportedFileType, ext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
// TrackMetadata holds all extracted tag data for an audio file.
|
||||
type TrackMetadata struct {
|
||||
// Basic info
|
||||
Title string
|
||||
Artist string
|
||||
Album string
|
||||
AlbumArtist string
|
||||
Composer string
|
||||
Genre string
|
||||
Year int
|
||||
|
||||
// Track position
|
||||
TrackNumber int
|
||||
TotalTracks int
|
||||
DiscNumber int
|
||||
TotalDiscs int
|
||||
|
||||
// Extended
|
||||
Lyrics string
|
||||
Comment string
|
||||
|
||||
// Cover art (if present)
|
||||
Picture *PictureData
|
||||
|
||||
// Format info
|
||||
TagFormat string // "ID3v2.3", "VORBIS", etc.
|
||||
FileFormat string // "MP3", "FLAC", etc.
|
||||
}
|
||||
|
||||
// PictureData holds embedded artwork.
|
||||
type PictureData struct {
|
||||
Data []byte
|
||||
MIMEType string
|
||||
Ext string // "jpg", "png", etc.
|
||||
}
|
||||
|
||||
// ExtractTags reads metadata tags from an audio file.
|
||||
func ExtractTags(path string) (*TrackMetadata, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open file for tag extraction: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return ExtractTagsFromReader(f)
|
||||
}
|
||||
|
||||
// ExtractTagsFromReader reads metadata from an io.ReadSeeker.
|
||||
func ExtractTagsFromReader(r io.ReadSeeker) (*TrackMetadata, error) {
|
||||
m, err := tag.ReadFrom(r)
|
||||
if err != nil {
|
||||
// No tags found is not necessarily an error - return empty metadata
|
||||
if errors.Is(err, tag.ErrNoTagsFound) {
|
||||
return &TrackMetadata{}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not read tags: %w", err)
|
||||
}
|
||||
|
||||
trackNum, totalTracks := m.Track()
|
||||
discNum, totalDiscs := m.Disc()
|
||||
|
||||
meta := &TrackMetadata{
|
||||
Title: m.Title(),
|
||||
Artist: m.Artist(),
|
||||
Album: m.Album(),
|
||||
AlbumArtist: m.AlbumArtist(),
|
||||
Composer: m.Composer(),
|
||||
Genre: m.Genre(),
|
||||
Year: m.Year(),
|
||||
TrackNumber: trackNum,
|
||||
TotalTracks: totalTracks,
|
||||
DiscNumber: discNum,
|
||||
TotalDiscs: totalDiscs,
|
||||
Lyrics: m.Lyrics(),
|
||||
Comment: m.Comment(),
|
||||
TagFormat: string(m.Format()),
|
||||
FileFormat: string(m.FileType()),
|
||||
}
|
||||
|
||||
// Extract picture if present
|
||||
if pic := m.Picture(); pic != nil {
|
||||
meta.Picture = &PictureData{
|
||||
Data: pic.Data,
|
||||
MIMEType: pic.MIMEType,
|
||||
Ext: pic.Ext,
|
||||
}
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
Reference in New Issue
Block a user