mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-03 16:50:40 +02:00
124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
const maxGeniusPageBytes = 8 << 20
|
|
|
|
func geniusLyricsContainer(node *html.Node) bool {
|
|
if node.Type != html.ElementNode || node.Data != "div" {
|
|
return false
|
|
}
|
|
for _, attr := range node.Attr {
|
|
if attr.Key == "data-lyrics-container" && attr.Val == "true" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func geniusExcludedNode(node *html.Node) bool {
|
|
for _, attr := range node.Attr {
|
|
if attr.Key == "data-exclude-from-selection" && attr.Val == "true" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func appendGeniusText(builder *strings.Builder, node *html.Node) {
|
|
if geniusExcludedNode(node) {
|
|
return
|
|
}
|
|
if node.Type == html.TextNode {
|
|
builder.WriteString(node.Data)
|
|
return
|
|
}
|
|
if node.Type == html.ElementNode && node.Data == "br" {
|
|
builder.WriteByte('\n')
|
|
return
|
|
}
|
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
appendGeniusText(builder, child)
|
|
}
|
|
}
|
|
|
|
func geniusLyricsFromHTML(body io.Reader) (string, error) {
|
|
document, err := html.Parse(body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse Genius page: %w", err)
|
|
}
|
|
var containers []*html.Node
|
|
var walk func(*html.Node)
|
|
walk = func(node *html.Node) {
|
|
if geniusLyricsContainer(node) {
|
|
containers = append(containers, node)
|
|
}
|
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
walk(child)
|
|
}
|
|
}
|
|
walk(document)
|
|
if len(containers) == 0 {
|
|
return "", lyricsNotFoundErrorf("Genius page has no lyrics container")
|
|
}
|
|
|
|
// Genius renders each verse/chorus as a separate lyrics container. Preserve
|
|
// document order and join all usable containers into one LRC/plain payload.
|
|
var sections []string
|
|
for _, container := range containers {
|
|
var builder strings.Builder
|
|
appendGeniusText(&builder, container)
|
|
candidate := strings.TrimSpace(strings.ReplaceAll(builder.String(), "\u00a0", " "))
|
|
if rawLyricsHasUsableContent(candidate) {
|
|
sections = append(sections, candidate)
|
|
}
|
|
}
|
|
if len(sections) == 0 {
|
|
return "", lyricsNotFoundErrorf("Genius page returned empty lyrics")
|
|
}
|
|
return strings.Join(sections, "\n"), nil
|
|
}
|
|
|
|
func (c *GeniusLyricsClient) fetchLyricsFromPage(pageURL string) (*LyricsResponse, error) {
|
|
pageURL = strings.TrimSpace(pageURL)
|
|
if pageURL == "" {
|
|
return nil, lyricsNotFoundErrorf("empty Genius lyrics URL")
|
|
}
|
|
req, err := http.NewRequest(http.MethodGet, pageURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Genius page request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "text/html,application/xhtml+xml")
|
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
|
req.Header.Set("User-Agent", getRandomUserAgent())
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Genius page request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, lyricsServiceUnavailableErrorf("Genius page returned HTTP %d", resp.StatusCode)
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(resp.Body, maxGeniusPageBytes+1))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read Genius page: %w", err)
|
|
}
|
|
if len(data) > maxGeniusPageBytes {
|
|
return nil, lyricsServiceUnavailableErrorf("Genius page exceeds %d bytes", maxGeniusPageBytes)
|
|
}
|
|
lrc, err := geniusLyricsFromHTML(strings.NewReader(string(data)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lyrics := lyricsResponseFromText(lrc, "Genius")
|
|
lyrics.Source = "Genius Direct"
|
|
return lyrics, nil
|
|
}
|