Squash merge audio-player-component into main

This commit is contained in:
2026-02-13 20:39:23 -06:00
parent 9b7cfd5bd1
commit d78c0584e2
122 changed files with 11750 additions and 1175 deletions
+36
View File
@@ -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)
}
}
+52
View File
@@ -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
}
+102
View File
@@ -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
}