Add album/track releases with audio analysis, AnalyzeAlbumRelease RPC, Docker path auto-resolution, release parsing decision tree
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TrackInfo struct {
|
||||
Format string
|
||||
BitDepth int
|
||||
SampleRate int
|
||||
Channels int
|
||||
DurationMs int
|
||||
BitrateKbps int
|
||||
IsLossless bool
|
||||
}
|
||||
|
||||
func Analyze(filePath string) (*TrackInfo, error) {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
|
||||
switch ext {
|
||||
case ".flac":
|
||||
return analyzeFLAC(filePath)
|
||||
case ".mp3":
|
||||
return analyzeMP3(filePath)
|
||||
case ".aac", ".m4a", ".ogg", ".wav", ".ape", ".wv", ".alac":
|
||||
return &TrackInfo{
|
||||
Format: strings.ToUpper(strings.TrimPrefix(ext, ".")),
|
||||
IsLossless: ext == ".wav" || ext == ".ape" || ext == ".wv" || ext == ".alac",
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported audio format: %s", ext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mewkiz/flac"
|
||||
)
|
||||
|
||||
func analyzeFLAC(filePath string) (*TrackInfo, error) {
|
||||
stream, err := flac.ParseFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing FLAC: %w", err)
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
info := stream.Info
|
||||
durationMs := int(info.NSamples * 1000 / uint64(info.SampleRate))
|
||||
|
||||
return &TrackInfo{
|
||||
Format: "FLAC",
|
||||
BitDepth: int(info.BitsPerSample),
|
||||
SampleRate: int(info.SampleRate),
|
||||
Channels: int(info.NChannels),
|
||||
DurationMs: durationMs,
|
||||
IsLossless: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/tcolgate/mp3"
|
||||
)
|
||||
|
||||
func analyzeMP3(filePath string) (*TrackInfo, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening MP3: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
decoder := mp3.NewDecoder(f)
|
||||
var frame mp3.Frame
|
||||
var skipped int
|
||||
var totalDuration time.Duration
|
||||
var sampleRate, channels, bitrate int
|
||||
var frameCount int
|
||||
|
||||
for {
|
||||
err := decoder.Decode(&frame, &skipped)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if frameCount == 0 {
|
||||
sampleRate = int(frame.Header().SampleRate())
|
||||
channels = channelCount(frame.Header().ChannelMode())
|
||||
bitrate = int(frame.Header().BitRate()) / 1000
|
||||
}
|
||||
totalDuration += frame.Duration()
|
||||
frameCount++
|
||||
}
|
||||
|
||||
return &TrackInfo{
|
||||
Format: "MP3",
|
||||
SampleRate: sampleRate,
|
||||
Channels: channels,
|
||||
DurationMs: int(totalDuration.Milliseconds()),
|
||||
BitrateKbps: bitrate,
|
||||
IsLossless: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func channelCount(mode mp3.FrameChannelMode) int {
|
||||
if mode == mp3.SingleChannel {
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
}
|
||||
Reference in New Issue
Block a user