refactor: move BPM fetching logic into provider package

This commit is contained in:
Mathis Maquenne
2025-09-10 15:09:24 +02:00
parent 523b562ed0
commit 0493866141
2 changed files with 32 additions and 27 deletions
+11 -10
View File
@@ -12,12 +12,12 @@ import (
"time" "time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/mathismqn/godeez/internal/bpm"
"github.com/mathismqn/godeez/internal/config" "github.com/mathismqn/godeez/internal/config"
"github.com/mathismqn/godeez/internal/crypto" "github.com/mathismqn/godeez/internal/crypto"
"github.com/mathismqn/godeez/internal/deezer" "github.com/mathismqn/godeez/internal/deezer"
"github.com/mathismqn/godeez/internal/fileutil" "github.com/mathismqn/godeez/internal/fileutil"
"github.com/mathismqn/godeez/internal/logger" "github.com/mathismqn/godeez/internal/logger"
"github.com/mathismqn/godeez/internal/provider"
"github.com/mathismqn/godeez/internal/store" "github.com/mathismqn/godeez/internal/store"
"github.com/mathismqn/godeez/internal/tags" "github.com/mathismqn/godeez/internal/tags"
) )
@@ -192,19 +192,20 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son
return nil, SkipError{Path: path} return nil, SkipError{Path: path}
} }
var metricsChan chan *bpm.Metrics var bpmChan chan provider.BPMKey
var errChan chan error var errChan chan error
if opts.BPM { if opts.BPM {
metricsChan = make(chan *bpm.Metrics, 1) bpmChan = make(chan provider.BPMKey, 1)
errChan = make(chan error, 1) errChan = make(chan error, 1)
go func() { go func() {
metrics, err := bpm.FetchMetrics(ctx, c.deezerClient.Session.HttpClient, song.Artist, song.Title, song.Duration) p := provider.BPMProvider{}
bpmKey, err := p.Fetch(ctx, c.deezerClient.Session.HttpClient, song.Artist, song.Title, song.Duration)
if err != nil { if err != nil {
errChan <- err errChan <- err
return return
} }
metricsChan <- metrics bpmChan <- bpmKey
}() }()
} }
@@ -230,10 +231,10 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son
warnings = append(warnings, fmt.Sprintf("requested quality '%s' not available, using '%s' instead", opts.Quality, strings.ToLower(mediaFormat))) warnings = append(warnings, fmt.Sprintf("requested quality '%s' not available, using '%s' instead", opts.Quality, strings.ToLower(mediaFormat)))
} }
metrics := &bpm.Metrics{} bpmKey := provider.BPMKey{}
if opts.BPM { if opts.BPM {
select { select {
case metrics = <-metricsChan: case bpmKey = <-bpmChan:
case err := <-errChan: case err := <-errChan:
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
warnings = append(warnings, fmt.Sprintf("failed to fetch BPM and key: %v", err)) warnings = append(warnings, fmt.Sprintf("failed to fetch BPM and key: %v", err))
@@ -246,7 +247,7 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son
warnings = append(warnings, fmt.Sprintf("failed to fetch cover image: %v", err)) warnings = append(warnings, fmt.Sprintf("failed to fetch cover image: %v", err))
} }
warnings = append(warnings, c.finalizeDownload(resource, song, outputPath, mediaFormat, cover, metrics)...) warnings = append(warnings, c.finalizeDownload(resource, song, outputPath, mediaFormat, cover, bpmKey)...)
return warnings, nil return warnings, nil
} }
@@ -308,10 +309,10 @@ func (c *Client) streamToFile(ctx context.Context, stream io.ReadCloser, outputP
return nil return nil
} }
func (c *Client) finalizeDownload(resource deezer.Resource, song *deezer.Song, outputPath, mediaFormat string, cover []byte, metrics *bpm.Metrics) []string { func (c *Client) finalizeDownload(resource deezer.Resource, song *deezer.Song, outputPath, mediaFormat string, cover []byte, bpmKey provider.BPMKey) []string {
var warnings []string var warnings []string
if err := tags.AddTags(resource, song, cover, outputPath, metrics.BPM, metrics.Key); err != nil { if err := tags.AddTags(resource, song, cover, outputPath, bpmKey.BPM, bpmKey.Key); err != nil {
warnings = append(warnings, fmt.Sprintf("failed to add tags: %v", err)) warnings = append(warnings, fmt.Sprintf("failed to add tags: %v", err))
} }
@@ -1,4 +1,4 @@
package bpm package provider
import ( import (
"bytes" "bytes"
@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" neturl "net/url"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -14,30 +14,32 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
) )
type Metrics struct { type BPMProvider struct{}
type BPMKey struct {
BPM string BPM string
Key string Key string
} }
func FetchMetrics(ctx context.Context, httpClient *http.Client, artist, title, duration string) (*Metrics, error) { func (p BPMProvider) Fetch(ctx context.Context, httpClient *http.Client, artist, title, duration string) (BPMKey, error) {
url, err := findSongURL(ctx, httpClient, artist, title, duration) url, err := p.findSongURL(ctx, httpClient, artist, title, duration)
if err != nil { if err != nil {
return nil, err return BPMKey{}, err
} }
html, err := fetchPage(ctx, httpClient, url) html, err := p.fetchPage(ctx, httpClient, url)
if err != nil { if err != nil {
return nil, err return BPMKey{}, err
} }
return parseMetrics(html) return p.parse(html)
} }
func findSongURL(ctx context.Context, httpClient *http.Client, artist, title, duration string) (string, error) { func (p BPMProvider) findSongURL(ctx context.Context, httpClient *http.Client, artist, title, duration string) (string, error) {
rootUrl := "https://songbpm.com" rootUrl := "https://songbpm.com"
reqUrl := rootUrl + "/searches" reqUrl := rootUrl + "/searches"
values := url.Values{} values := neturl.Values{}
values.Add("query", fmt.Sprintf("%s %s", artist, title)) values.Add("query", fmt.Sprintf("%s %s", artist, title))
req, err := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBufferString(values.Encode())) req, err := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBufferString(values.Encode()))
@@ -90,12 +92,13 @@ func findSongURL(ctx context.Context, httpClient *http.Client, artist, title, du
} }
foundDuration := minutes*60 + seconds foundDuration := minutes*60 + seconds
duration, err := strconv.Atoi(duration) wantDuration, err := strconv.Atoi(duration)
if err != nil { if err != nil {
return true return true
} }
if foundDuration <= (duration-2) || foundDuration >= (duration+2) { const durationToleranceSec = 2
if foundDuration <= (wantDuration-durationToleranceSec) || foundDuration >= (wantDuration+durationToleranceSec) {
return true return true
} }
@@ -112,7 +115,7 @@ func findSongURL(ctx context.Context, httpClient *http.Client, artist, title, du
return rootUrl + url, nil return rootUrl + url, nil
} }
func fetchPage(ctx context.Context, httpClient *http.Client, url string) (string, error) { func (p BPMProvider) fetchPage(ctx context.Context, httpClient *http.Client, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
return "", err return "", err
@@ -136,7 +139,7 @@ func fetchPage(ctx context.Context, httpClient *http.Client, url string) (string
return string(body), nil return string(body), nil
} }
func parseMetrics(html string) (*Metrics, error) { func (p BPMProvider) parse(html string) (BPMKey, error) {
bpmRegex := regexp.MustCompile(`tempo of <span[^>]*>(\d+) BPM`) bpmRegex := regexp.MustCompile(`tempo of <span[^>]*>(\d+) BPM`)
bpmMatch := bpmRegex.FindStringSubmatch(html) bpmMatch := bpmRegex.FindStringSubmatch(html)
@@ -147,7 +150,7 @@ func parseMetrics(html string) (*Metrics, error) {
modeMatch := modeRegex.FindStringSubmatch(html) modeMatch := modeRegex.FindStringSubmatch(html)
if len(bpmMatch) != 2 || len(keyMatch) != 2 || len(modeMatch) != 2 { if len(bpmMatch) != 2 || len(keyMatch) != 2 || len(modeMatch) != 2 {
return nil, fmt.Errorf("no data found") return BPMKey{}, fmt.Errorf("no data found")
} }
isMinor := false isMinor := false
@@ -169,8 +172,9 @@ func parseMetrics(html string) (*Metrics, error) {
key += "m" key += "m"
} }
return &Metrics{ return BPMKey{
BPM: bpm, BPM: bpm,
Key: key, Key: key,
}, nil }, nil
} }