refactor: simplify codebase (-209 lines)

This commit is contained in:
Mathis Maquenne
2026-03-01 21:55:42 +01:00
parent cd45c3f40a
commit 08b608ec80
31 changed files with 389 additions and 598 deletions
+38 -59
View File
@@ -14,40 +14,43 @@ import (
"github.com/PuerkitoBio/goquery"
)
type BPMProvider struct{}
type BPMKey struct {
BPM string
Key string
}
func (p BPMProvider) Fetch(ctx context.Context, httpClient *http.Client, artist, title, duration string) (BPMKey, error) {
url, err := p.findSongURL(ctx, httpClient, artist, title, duration)
var (
bpmRegex = regexp.MustCompile(`tempo of <span[^>]*>(\d+) BPM`)
keyRegex = regexp.MustCompile(`with a <span[^>]*>([A-G](?:♯|#|♭|b)?(?:/[A-G](?:♯|#|♭|b)?)?)</span> key`)
modeRegex = regexp.MustCompile(`a <span[^>]*>([a-z]+)</span> mode`)
)
func FetchBPM(ctx context.Context, httpClient *http.Client, artist, title, duration string) (BPMKey, error) {
songURL, err := findSongURL(ctx, httpClient, artist, title, duration)
if err != nil {
return BPMKey{}, err
}
html, err := p.fetchPage(ctx, httpClient, url)
html, err := fetchBPMPage(ctx, httpClient, songURL)
if err != nil {
return BPMKey{}, err
}
return p.parse(html)
return parseBPM(html)
}
func (p BPMProvider) findSongURL(ctx context.Context, httpClient *http.Client, artist, title, duration string) (string, error) {
rootUrl := "https://songbpm.com"
reqUrl := rootUrl + "/searches"
func findSongURL(ctx context.Context, httpClient *http.Client, artist, title, duration string) (string, error) {
const rootURL = "https://songbpm.com"
values := neturl.Values{}
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", rootURL+"/searches", bytes.NewBufferString(values.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Origin", "https://songbpm.com")
req.Header.Set("Origin", rootURL)
resp, err := httpClient.Do(req)
if err != nil {
@@ -64,20 +67,22 @@ func (p BPMProvider) findSongURL(ctx context.Context, httpClient *http.Client, a
return "", err
}
var (
found bool
url string
)
wantDuration, err := strconv.Atoi(duration)
if err != nil {
return "", fmt.Errorf("invalid duration: %w", err)
}
doc.Find("a.flex.flex-col").EachWithBreak(func(_ int, selection *goquery.Selection) bool {
lowerSelection := strings.ToLower(selection.Text())
lowerTitle := strings.ToLower(title)
lowerArtist := strings.ToLower(artist)
if !strings.Contains(lowerSelection, lowerTitle) || !strings.Contains(lowerSelection, lowerArtist) {
lowerTitle := strings.ToLower(title)
lowerArtist := strings.ToLower(artist)
var matchURL string
doc.Find("a.flex.flex-col").EachWithBreak(func(_ int, sel *goquery.Selection) bool {
text := strings.ToLower(sel.Text())
if !strings.Contains(text, lowerTitle) || !strings.Contains(text, lowerArtist) {
return true
}
durationStr := strings.TrimSpace(selection.Find("div.flex-1.flex-col.items-center").Eq(1).Find("span.text-2xl").Text())
durationStr := strings.TrimSpace(sel.Find("div.flex-1.flex-col.items-center").Eq(1).Find("span.text-2xl").Text())
parts := strings.Split(durationStr, ":")
if len(parts) != 2 {
return true
@@ -91,31 +96,24 @@ func (p BPMProvider) findSongURL(ctx context.Context, httpClient *http.Client, a
return true
}
const toleranceSec = 2
foundDuration := minutes*60 + seconds
wantDuration, err := strconv.Atoi(duration)
if err != nil {
if foundDuration <= wantDuration-toleranceSec || foundDuration >= wantDuration+toleranceSec {
return true
}
const durationToleranceSec = 2
if foundDuration <= (wantDuration-durationToleranceSec) || foundDuration >= (wantDuration+durationToleranceSec) {
return true
}
url = selection.AttrOr("href", "")
found = true
matchURL = sel.AttrOr("href", "")
return false
})
if !found {
if matchURL == "" {
return "", fmt.Errorf("no data found")
}
return rootUrl + url, nil
return rootURL + matchURL, nil
}
func (p BPMProvider) fetchPage(ctx context.Context, httpClient *http.Client, url string) (string, error) {
func fetchBPMPage(ctx context.Context, httpClient *http.Client, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
@@ -139,42 +137,23 @@ func (p BPMProvider) fetchPage(ctx context.Context, httpClient *http.Client, url
return string(body), nil
}
func (p BPMProvider) parse(html string) (BPMKey, error) {
bpmRegex := regexp.MustCompile(`tempo of <span[^>]*>(\d+) BPM`)
func parseBPM(html string) (BPMKey, error) {
bpmMatch := bpmRegex.FindStringSubmatch(html)
keyRegex := regexp.MustCompile(`with a <span[^>]*>([A-G](?:♯|#|♭|b)?(?:/[A-G](?:♯|#|♭|b)?)?)</span> key`)
keyMatch := keyRegex.FindStringSubmatch(html)
modeRegex := regexp.MustCompile(`a <span[^>]*>([a-z]+)</span> mode`)
modeMatch := modeRegex.FindStringSubmatch(html)
if len(bpmMatch) != 2 || len(keyMatch) != 2 || len(modeMatch) != 2 {
return BPMKey{}, fmt.Errorf("no data found")
}
isMinor := false
bpm := bpmMatch[1]
key := keyMatch[1]
key := strings.SplitN(keyMatch[1], "/", 2)[0]
key = strings.ReplaceAll(key, "\u266f", "#")
key = strings.ReplaceAll(key, "\u266d", "b")
if modeMatch[1] == "minor" {
isMinor = true
}
if strings.Contains(key, "/") {
parts := strings.Split(key, "/")
key = parts[0]
}
key = strings.ReplaceAll(key, "♯", "#")
key = strings.ReplaceAll(key, "♭", "b")
if isMinor && !strings.HasSuffix(key, "m") {
key += "m"
}
return BPMKey{
BPM: bpm,
Key: key,
}, nil
return BPMKey{BPM: bpm, Key: key}, nil
}
+45 -69
View File
@@ -9,48 +9,50 @@ import (
"github.com/PuerkitoBio/goquery"
)
var electronicKeywords = []string{
var electronicKeywords = toLower([]string{
"Ambient", "Bass", "Big Room", "Breakbeat", "Dance", "Disco", "Downtempo",
"Drum And Bass", "Dub", "Dubstep", "EDM", "Electro", "Electronic", "Electronica",
"Eurodance", "Gabber", "Garage", "Hardcore", "Hardstyle", "House", "Industrial",
"Jungle", "Moombahton", "Synthpop", "Synthwave", "Techno", "Trance", "Trap",
"Trip Hop", "Vaporwave",
}
})
var nonElectronicKeywords = []string{
var nonElectronicKeywords = toLower([]string{
"Blues", "Chillout", "Classical", "Country", "Folk", "Funk", "Hip Hop", "Jazz",
"Latin", "Metal", "Pop", "R&B", "Rap", "Reggae", "Rock", "Soul",
})
func toLower(ss []string) []string {
out := make([]string, len(ss))
for i, s := range ss {
out[i] = strings.ToLower(s)
}
return out
}
type GenreProvider struct{}
func FetchGenre(ctx context.Context, httpClient *http.Client, artist, title string) (string, error) {
reqURL := fmt.Sprintf("https://www.last.fm/music/%s/%s/+tags", artist, title)
func (p GenreProvider) Fetch(ctx context.Context, httpClient *http.Client, artist, title string) (string, error) {
reqUrl := fmt.Sprintf("https://www.last.fm/music/%s/%s/+tags", artist, title)
doc, err := p.fetchPage(ctx, httpClient, reqUrl)
doc, err := fetchGenrePage(ctx, httpClient, reqURL)
if err != nil {
return "", err
}
tags := p.parse(doc)
if len(tags) == 0 {
return "", fmt.Errorf("no data found")
}
tags := parseGenreTags(doc)
if len(tags) > 2 {
tags = tags[:2]
}
filteredTags := p.filterTags(tags)
if len(filteredTags) == 0 {
filtered := filterTags(tags)
if len(filtered) == 0 {
return "", fmt.Errorf("no data found")
}
genre := p.formatTags(filteredTags)
return genre, nil
return formatTags(filtered), nil
}
func (p GenreProvider) fetchPage(ctx context.Context, httpClient *http.Client, reqUrl string) (*goquery.Document, error) {
req, err := http.NewRequestWithContext(ctx, "GET", reqUrl, nil)
func fetchGenrePage(ctx context.Context, httpClient *http.Client, url string) (*goquery.Document, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
@@ -65,84 +67,58 @@ func (p GenreProvider) fetchPage(ctx context.Context, httpClient *http.Client, r
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, err
}
return doc, nil
return goquery.NewDocumentFromReader(resp.Body)
}
func (p GenreProvider) parse(doc *goquery.Document) []string {
func parseGenreTags(doc *goquery.Document) []string {
var tags []string
doc.Find("ol.big-tags .big-tags-item-name a").Each(func(_ int, s *goquery.Selection) {
tag := strings.TrimSpace(s.Text())
if tag != "" {
if tag := strings.TrimSpace(s.Text()); tag != "" {
tags = append(tags, tag)
}
})
return tags
}
func (p GenreProvider) filterTags(tags []string) []string {
var electronicTags []string
var nonElectronicTags []string
func matchesKeyword(tag string, keywords []string) bool {
tagLower := strings.ToLower(tag)
for _, kw := range keywords {
if strings.Contains(tagLower, kw) {
return true
}
}
return false
}
func filterTags(tags []string) []string {
var electronic, nonElectronic []string
for _, tag := range tags {
if p.isElectronicGenre(tag) {
electronicTags = append(electronicTags, tag)
} else if p.isNonElectronicGenre(tag) {
nonElectronicTags = append(nonElectronicTags, tag)
if matchesKeyword(tag, electronicKeywords) {
electronic = append(electronic, tag)
} else if matchesKeyword(tag, nonElectronicKeywords) {
nonElectronic = append(nonElectronic, tag)
}
}
var filteredTags []string
filteredTags = append(filteredTags, electronicTags...)
if len(electronicTags) > 0 {
filteredTags = append(filteredTags, nonElectronicTags...)
if len(electronic) > 0 {
return append(electronic, nonElectronic...)
}
return filteredTags
return electronic
}
func (p GenreProvider) isElectronicGenre(tag string) bool {
tagLower := strings.ToLower(tag)
for _, allowed := range electronicKeywords {
if strings.Contains(tagLower, strings.ToLower(allowed)) {
return true
}
}
return false
}
func (p GenreProvider) isNonElectronicGenre(tag string) bool {
tagLower := strings.ToLower(tag)
for _, allowed := range nonElectronicKeywords {
if strings.Contains(tagLower, strings.ToLower(allowed)) {
return true
}
}
return false
}
func (p GenreProvider) formatTags(tags []string) string {
var formatted []string
func formatTags(tags []string) string {
formatted := make([]string, 0, len(tags))
for _, tag := range tags {
tag = strings.TrimSpace(tag)
if tag == "" {
continue
}
words := strings.Fields(tag)
for i, w := range words {
if len(w) > 0 {
words[i] = strings.ToUpper(string(w[0])) + strings.ToLower(w[1:])
}
words[i] = strings.ToUpper(w[:1]) + strings.ToLower(w[1:])
}
formatted = append(formatted, strings.Join(words, " "))
}
return strings.Join(formatted, " / ")
}