mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-25 02:50:43 +02:00
refactor: extract shared helpers for repeated low-level patterns
Dart: - notification_service: single _details() builder replaces 13 copies of the NotificationDetails block - platform_bridge: _invokeMap() for 34 invoke+decode call sites, _cachedInvoke() unifies the three TTL/in-flight cache scaffolds - ffmpeg_service: _promoteTempOutput(), _appendCoverInputArgs(), single _writeReplayGainTags() and _convertToLossless() for the ALAC/FLAC twins - sqlite_helpers.dart: shared openAppDatabase/path-key/migration helpers for the three database classes - library_collections: parametrized wishlist/loved/favorite CRUD - extension_provider: one predicate-based replacedBuiltIn* lookup Go: - extension runtime: parseGojaHeaders/coerceGojaBody/doExtensionHTTP shared by httpGet/httpPost/httpRequest/shortcuts/fetch - exports_metadata: applyAudioMetadataToResult + successMethodJSON, APE edit path reuses audioMetadataFromEditFields - lyrics: lrclibGet() for both LRCLib fetchers - extension_store: drop hand-rolled strings helpers
This commit is contained in:
+45
-190
@@ -9,6 +9,36 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func applyAudioMetadataToResult(result map[string]any, meta *AudioMetadata) {
|
||||
result["title"] = meta.Title
|
||||
result["artist"] = meta.Artist
|
||||
result["album"] = meta.Album
|
||||
result["album_artist"] = meta.AlbumArtist
|
||||
result["date"] = meta.Date
|
||||
if meta.Date == "" {
|
||||
result["date"] = meta.Year
|
||||
}
|
||||
result["track_number"] = meta.TrackNumber
|
||||
result["total_tracks"] = meta.TotalTracks
|
||||
result["disc_number"] = meta.DiscNumber
|
||||
result["total_discs"] = meta.TotalDiscs
|
||||
result["isrc"] = meta.ISRC
|
||||
result["lyrics"] = meta.Lyrics
|
||||
result["genre"] = meta.Genre
|
||||
result["label"] = meta.Label
|
||||
result["copyright"] = meta.Copyright
|
||||
result["composer"] = meta.Composer
|
||||
result["comment"] = meta.Comment
|
||||
result["replaygain_track_gain"] = meta.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = meta.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = meta.ReplayGainAlbumGain
|
||||
result["replaygain_album_peak"] = meta.ReplayGainAlbumPeak
|
||||
}
|
||||
|
||||
func successMethodJSON(method string) (string, error) {
|
||||
return marshalJSONString(map[string]any{"success": true, "method": method})
|
||||
}
|
||||
|
||||
func ReadFileMetadata(filePath string) (string, error) {
|
||||
lower := strings.ToLower(filePath)
|
||||
isFlac := strings.HasSuffix(lower, ".flac")
|
||||
@@ -121,29 +151,7 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
result["format"] = "m4a"
|
||||
meta, err := ReadM4ATags(filePath)
|
||||
if err == nil && meta != nil {
|
||||
result["title"] = meta.Title
|
||||
result["artist"] = meta.Artist
|
||||
result["album"] = meta.Album
|
||||
result["album_artist"] = meta.AlbumArtist
|
||||
result["date"] = meta.Date
|
||||
if meta.Date == "" {
|
||||
result["date"] = meta.Year
|
||||
}
|
||||
result["track_number"] = meta.TrackNumber
|
||||
result["total_tracks"] = meta.TotalTracks
|
||||
result["disc_number"] = meta.DiscNumber
|
||||
result["total_discs"] = meta.TotalDiscs
|
||||
result["isrc"] = meta.ISRC
|
||||
result["lyrics"] = meta.Lyrics
|
||||
result["genre"] = meta.Genre
|
||||
result["label"] = meta.Label
|
||||
result["copyright"] = meta.Copyright
|
||||
result["composer"] = meta.Composer
|
||||
result["comment"] = meta.Comment
|
||||
result["replaygain_track_gain"] = meta.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = meta.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = meta.ReplayGainAlbumGain
|
||||
result["replaygain_album_peak"] = meta.ReplayGainAlbumPeak
|
||||
applyAudioMetadataToResult(result, meta)
|
||||
}
|
||||
quality, qualityErr := GetM4AQuality(filePath)
|
||||
if qualityErr == nil {
|
||||
@@ -163,29 +171,7 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
result["audio_codec"] = "mp3"
|
||||
meta, err := ReadID3Tags(filePath)
|
||||
if err == nil && meta != nil {
|
||||
result["title"] = meta.Title
|
||||
result["artist"] = meta.Artist
|
||||
result["album"] = meta.Album
|
||||
result["album_artist"] = meta.AlbumArtist
|
||||
result["date"] = meta.Date
|
||||
if meta.Date == "" {
|
||||
result["date"] = meta.Year
|
||||
}
|
||||
result["track_number"] = meta.TrackNumber
|
||||
result["total_tracks"] = meta.TotalTracks
|
||||
result["disc_number"] = meta.DiscNumber
|
||||
result["total_discs"] = meta.TotalDiscs
|
||||
result["isrc"] = meta.ISRC
|
||||
result["lyrics"] = meta.Lyrics
|
||||
result["genre"] = meta.Genre
|
||||
result["label"] = meta.Label
|
||||
result["copyright"] = meta.Copyright
|
||||
result["composer"] = meta.Composer
|
||||
result["comment"] = meta.Comment
|
||||
result["replaygain_track_gain"] = meta.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = meta.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = meta.ReplayGainAlbumGain
|
||||
result["replaygain_album_peak"] = meta.ReplayGainAlbumPeak
|
||||
applyAudioMetadataToResult(result, meta)
|
||||
}
|
||||
quality, qualityErr := GetMP3Quality(filePath)
|
||||
if qualityErr == nil {
|
||||
@@ -201,29 +187,7 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
result["audio_codec"] = "opus"
|
||||
meta, err := ReadOggVorbisComments(filePath)
|
||||
if err == nil && meta != nil {
|
||||
result["title"] = meta.Title
|
||||
result["artist"] = meta.Artist
|
||||
result["album"] = meta.Album
|
||||
result["album_artist"] = meta.AlbumArtist
|
||||
result["date"] = meta.Date
|
||||
if meta.Date == "" {
|
||||
result["date"] = meta.Year
|
||||
}
|
||||
result["track_number"] = meta.TrackNumber
|
||||
result["total_tracks"] = meta.TotalTracks
|
||||
result["disc_number"] = meta.DiscNumber
|
||||
result["total_discs"] = meta.TotalDiscs
|
||||
result["isrc"] = meta.ISRC
|
||||
result["lyrics"] = meta.Lyrics
|
||||
result["genre"] = meta.Genre
|
||||
result["label"] = meta.Label
|
||||
result["copyright"] = meta.Copyright
|
||||
result["composer"] = meta.Composer
|
||||
result["comment"] = meta.Comment
|
||||
result["replaygain_track_gain"] = meta.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = meta.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = meta.ReplayGainAlbumGain
|
||||
result["replaygain_album_peak"] = meta.ReplayGainAlbumPeak
|
||||
applyAudioMetadataToResult(result, meta)
|
||||
}
|
||||
quality, qualityErr := GetOggQuality(filePath)
|
||||
if qualityErr == nil {
|
||||
@@ -240,29 +204,7 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
if apeErr == nil && apeTag != nil {
|
||||
meta := APETagToAudioMetadata(apeTag)
|
||||
if meta != nil {
|
||||
result["title"] = meta.Title
|
||||
result["artist"] = meta.Artist
|
||||
result["album"] = meta.Album
|
||||
result["album_artist"] = meta.AlbumArtist
|
||||
result["date"] = meta.Date
|
||||
if meta.Date == "" {
|
||||
result["date"] = meta.Year
|
||||
}
|
||||
result["track_number"] = meta.TrackNumber
|
||||
result["total_tracks"] = meta.TotalTracks
|
||||
result["disc_number"] = meta.DiscNumber
|
||||
result["total_discs"] = meta.TotalDiscs
|
||||
result["isrc"] = meta.ISRC
|
||||
result["lyrics"] = meta.Lyrics
|
||||
result["genre"] = meta.Genre
|
||||
result["label"] = meta.Label
|
||||
result["copyright"] = meta.Copyright
|
||||
result["composer"] = meta.Composer
|
||||
result["comment"] = meta.Comment
|
||||
result["replaygain_track_gain"] = meta.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = meta.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = meta.ReplayGainAlbumGain
|
||||
result["replaygain_album_peak"] = meta.ReplayGainAlbumPeak
|
||||
applyAudioMetadataToResult(result, meta)
|
||||
}
|
||||
}
|
||||
} else if isWav || isAiff {
|
||||
@@ -281,29 +223,7 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
quality, qualityErr = GetWAVQuality(filePath)
|
||||
}
|
||||
if meta != nil {
|
||||
result["title"] = meta.Title
|
||||
result["artist"] = meta.Artist
|
||||
result["album"] = meta.Album
|
||||
result["album_artist"] = meta.AlbumArtist
|
||||
result["date"] = meta.Date
|
||||
if meta.Date == "" {
|
||||
result["date"] = meta.Year
|
||||
}
|
||||
result["track_number"] = meta.TrackNumber
|
||||
result["total_tracks"] = meta.TotalTracks
|
||||
result["disc_number"] = meta.DiscNumber
|
||||
result["total_discs"] = meta.TotalDiscs
|
||||
result["isrc"] = meta.ISRC
|
||||
result["lyrics"] = meta.Lyrics
|
||||
result["genre"] = meta.Genre
|
||||
result["label"] = meta.Label
|
||||
result["copyright"] = meta.Copyright
|
||||
result["composer"] = meta.Composer
|
||||
result["comment"] = meta.Comment
|
||||
result["replaygain_track_gain"] = meta.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = meta.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = meta.ReplayGainAlbumGain
|
||||
result["replaygain_album_peak"] = meta.ReplayGainAlbumPeak
|
||||
applyAudioMetadataToResult(result, meta)
|
||||
}
|
||||
if qualityErr == nil && quality != nil {
|
||||
result["bit_depth"] = quality.BitDepth
|
||||
@@ -376,9 +296,7 @@ func WriteM4AFreeformTags(filePath, metadataJSON string) (string, error) {
|
||||
return "", fmt.Errorf("failed to write M4A freeform tags: %w", err)
|
||||
}
|
||||
|
||||
resp := map[string]any{"success": true, "method": "native_m4a_freeform"}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_m4a_freeform")
|
||||
}
|
||||
|
||||
// EnsureAC4Config normalizes a decrypted AC-4 file to a standards-compliant ISO
|
||||
@@ -424,12 +342,7 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
return "", fmt.Errorf("failed to write M4A metadata: %w", err)
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"success": true,
|
||||
"method": "native_m4a_replaygain",
|
||||
}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_m4a_replaygain")
|
||||
}
|
||||
|
||||
if isFlac {
|
||||
@@ -446,12 +359,7 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
return "", fmt.Errorf("failed to write FLAC metadata: %w", err)
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"success": true,
|
||||
"method": "native",
|
||||
}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native")
|
||||
}
|
||||
|
||||
// WAV / AIFF: write tags into an embedded ID3v2.4 chunk natively.
|
||||
@@ -459,59 +367,17 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
if err := WriteWAVTags(filePath, fields); err != nil {
|
||||
return "", fmt.Errorf("failed to write WAV metadata: %w", err)
|
||||
}
|
||||
resp := map[string]any{"success": true, "method": "native_wav"}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_wav")
|
||||
}
|
||||
if isAiffFile {
|
||||
if err := WriteAIFFTags(filePath, fields); err != nil {
|
||||
return "", fmt.Errorf("failed to write AIFF metadata: %w", err)
|
||||
}
|
||||
resp := map[string]any{"success": true, "method": "native_aiff"}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_aiff")
|
||||
}
|
||||
|
||||
if isApeFile {
|
||||
trackNum := 0
|
||||
totalTracks := 0
|
||||
discNum := 0
|
||||
totalDiscs := 0
|
||||
if v, ok := fields["track_number"]; ok && v != "" {
|
||||
fmt.Sscanf(v, "%d", &trackNum)
|
||||
}
|
||||
if v, ok := fields["track_total"]; ok && v != "" {
|
||||
fmt.Sscanf(v, "%d", &totalTracks)
|
||||
}
|
||||
if v, ok := fields["disc_number"]; ok && v != "" {
|
||||
fmt.Sscanf(v, "%d", &discNum)
|
||||
}
|
||||
if v, ok := fields["disc_total"]; ok && v != "" {
|
||||
fmt.Sscanf(v, "%d", &totalDiscs)
|
||||
}
|
||||
|
||||
meta := &AudioMetadata{
|
||||
Title: fields["title"],
|
||||
Artist: fields["artist"],
|
||||
Album: fields["album"],
|
||||
AlbumArtist: fields["album_artist"],
|
||||
Date: fields["date"],
|
||||
TrackNumber: trackNum,
|
||||
TotalTracks: totalTracks,
|
||||
DiscNumber: discNum,
|
||||
TotalDiscs: totalDiscs,
|
||||
ISRC: fields["isrc"],
|
||||
Lyrics: fields["lyrics"],
|
||||
Genre: fields["genre"],
|
||||
Label: fields["label"],
|
||||
Copyright: fields["copyright"],
|
||||
Composer: fields["composer"],
|
||||
Comment: fields["comment"],
|
||||
ReplayGainTrackGain: fields["replaygain_track_gain"],
|
||||
ReplayGainTrackPeak: fields["replaygain_track_peak"],
|
||||
ReplayGainAlbumGain: fields["replaygain_album_gain"],
|
||||
ReplayGainAlbumPeak: fields["replaygain_album_peak"],
|
||||
}
|
||||
meta := audioMetadataFromEditFields(fields)
|
||||
|
||||
newItems := AudioMetadataToAPEItems(meta)
|
||||
|
||||
@@ -560,12 +426,7 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
return "", fmt.Errorf("failed to write APE tags: %w", err)
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"success": true,
|
||||
"method": "native_ape",
|
||||
}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_ape")
|
||||
}
|
||||
|
||||
// MP3, Ogg/Opus, and M4A have native editors that preserve foreign
|
||||
@@ -578,27 +439,21 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
if err := EditMP3Fields(filePath, fields); err != nil {
|
||||
GoLog("[Metadata] Native MP3 edit failed, falling back to ffmpeg: %v\n", err)
|
||||
} else {
|
||||
resp := map[string]any{"success": true, "method": "native_mp3"}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_mp3")
|
||||
}
|
||||
}
|
||||
if isOggFile {
|
||||
if err := EditOggFields(filePath, fields); err != nil {
|
||||
GoLog("[Metadata] Native Ogg edit failed, falling back to ffmpeg: %v\n", err)
|
||||
} else {
|
||||
resp := map[string]any{"success": true, "method": "native_ogg"}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_ogg")
|
||||
}
|
||||
}
|
||||
if isM4AFile || isMP4ContainerFile(filePath) {
|
||||
if err := EditM4AFields(filePath, fields); err != nil {
|
||||
GoLog("[Metadata] Native M4A edit failed, falling back to ffmpeg: %v\n", err)
|
||||
} else {
|
||||
resp := map[string]any{"success": true, "method": "native_m4a"}
|
||||
s, _ := marshalJSONString(resp)
|
||||
return s, nil
|
||||
return successMethodJSON("native_m4a")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,126 +74,86 @@ func (r *extensionRuntime) validateDomain(urlStr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpGet(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": "URL is required",
|
||||
})
|
||||
}
|
||||
|
||||
urlStr := call.Arguments[0].String()
|
||||
|
||||
if err := r.validateDomain(urlStr); err != nil {
|
||||
GoLog("[Extension:%s] HTTP blocked: %v\n", r.extensionID, err)
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// parseGojaHeaders converts an exported goja value (expected map[string]any)
|
||||
// into string headers. Non-map values yield an empty map.
|
||||
func parseGojaHeaders(v any) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
headersObj := call.Arguments[1].Export()
|
||||
if h, ok := headersObj.(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
if h, ok := v.(map[string]any); ok {
|
||||
for k, val := range h {
|
||||
headers[k] = fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
req = r.bindDownloadCancelContext(req)
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
setDefaultExtensionUA(req)
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
respHeaders := make(map[string]any)
|
||||
for k, v := range resp.Header {
|
||||
if len(v) == 1 {
|
||||
respHeaders[k] = v[0]
|
||||
} else {
|
||||
respHeaders[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"statusCode": resp.StatusCode,
|
||||
"status": resp.StatusCode,
|
||||
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||
"url": resp.Request.URL.String(),
|
||||
"body": string(body),
|
||||
"headers": respHeaders,
|
||||
})
|
||||
return headers
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpPost(call goja.FunctionCall) goja.Value {
|
||||
// coerceExportedBody stringifies a request body already exported from goja:
|
||||
// strings pass through, maps/arrays are JSON-encoded, anything else is %v.
|
||||
func coerceExportedBody(v any) (string, error) {
|
||||
switch b := v.(type) {
|
||||
case string:
|
||||
return b, nil
|
||||
case map[string]any, []any:
|
||||
jsonBytes, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to stringify body: %v", err)
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
default:
|
||||
return fmt.Sprintf("%v", b), nil
|
||||
}
|
||||
}
|
||||
|
||||
// coerceGojaBody is coerceExportedBody for a raw goja argument; undefined/null
|
||||
// yield "", and the fallback uses goja's own String() conversion.
|
||||
func coerceGojaBody(v goja.Value) (string, error) {
|
||||
if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
|
||||
return "", nil
|
||||
}
|
||||
switch b := v.Export().(type) {
|
||||
case string:
|
||||
return b, nil
|
||||
case map[string]any, []any:
|
||||
return coerceExportedBody(b)
|
||||
default:
|
||||
return v.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func flattenHTTPHeaders(h http.Header) map[string]any {
|
||||
flat := make(map[string]any, len(h))
|
||||
for k, v := range h {
|
||||
if len(v) == 1 {
|
||||
flat[k] = v[0]
|
||||
} else {
|
||||
flat[k] = v
|
||||
}
|
||||
}
|
||||
return flat
|
||||
}
|
||||
|
||||
// checkExtensionURL extracts and allowlist-validates the URL argument.
|
||||
// On failure it returns a non-nil error value to hand back to JS.
|
||||
func (r *extensionRuntime) checkExtensionURL(call goja.FunctionCall) (string, goja.Value) {
|
||||
if len(call.Arguments) < 1 {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
return "", r.vm.ToValue(map[string]any{
|
||||
"error": "URL is required",
|
||||
})
|
||||
}
|
||||
|
||||
urlStr := call.Arguments[0].String()
|
||||
|
||||
if err := r.validateDomain(urlStr); err != nil {
|
||||
GoLog("[Extension:%s] HTTP blocked: %v\n", r.extensionID, err)
|
||||
return r.vm.ToValue(map[string]any{
|
||||
return "", r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return urlStr, nil
|
||||
}
|
||||
|
||||
var bodyStr string
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
bodyArg := call.Arguments[1].Export()
|
||||
switch v := bodyArg.(type) {
|
||||
case string:
|
||||
bodyStr = v
|
||||
case map[string]any, []any:
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": fmt.Sprintf("failed to stringify body: %v", err),
|
||||
})
|
||||
}
|
||||
bodyStr = string(jsonBytes)
|
||||
default:
|
||||
bodyStr = call.Arguments[1].String()
|
||||
}
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) {
|
||||
headersObj := call.Arguments[2].Export()
|
||||
if h, ok := headersObj.(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", urlStr, strings.NewReader(bodyStr))
|
||||
// doExtensionHTTP builds and executes the request, returning the extension
|
||||
// response map (or {"error": ...}). defaultJSON sets Content-Type
|
||||
// application/json when the caller did not provide one.
|
||||
func (r *extensionRuntime) doExtensionHTTP(method, urlStr string, body io.Reader, defaultJSON bool, headers map[string]string) goja.Value {
|
||||
req, err := http.NewRequest(method, urlStr, body)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
@@ -204,9 +164,8 @@ func (r *extensionRuntime) httpPost(call goja.FunctionCall) goja.Value {
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
setDefaultExtensionUA(req)
|
||||
if req.Header.Get("Content-Type") == "" {
|
||||
if defaultJSON && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
@@ -218,138 +177,78 @@ func (r *extensionRuntime) httpPost(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
respBody, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
respHeaders := make(map[string]any)
|
||||
for k, v := range resp.Header {
|
||||
if len(v) == 1 {
|
||||
respHeaders[k] = v[0]
|
||||
} else {
|
||||
respHeaders[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"statusCode": resp.StatusCode,
|
||||
"status": resp.StatusCode,
|
||||
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||
"url": resp.Request.URL.String(),
|
||||
"body": string(body),
|
||||
"headers": respHeaders,
|
||||
"body": string(respBody),
|
||||
"headers": flattenHTTPHeaders(resp.Header),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpRequest(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": "URL is required",
|
||||
})
|
||||
func (r *extensionRuntime) httpGet(call goja.FunctionCall) goja.Value {
|
||||
urlStr, errVal := r.checkExtensionURL(call)
|
||||
if errVal != nil {
|
||||
return errVal
|
||||
}
|
||||
headers := parseGojaHeaders(call.Argument(1).Export())
|
||||
return r.doExtensionHTTP("GET", urlStr, nil, false, headers)
|
||||
}
|
||||
|
||||
urlStr := call.Arguments[0].String()
|
||||
|
||||
if err := r.validateDomain(urlStr); err != nil {
|
||||
GoLog("[Extension:%s] HTTP blocked: %v\n", r.extensionID, err)
|
||||
func (r *extensionRuntime) httpPost(call goja.FunctionCall) goja.Value {
|
||||
urlStr, errVal := r.checkExtensionURL(call)
|
||||
if errVal != nil {
|
||||
return errVal
|
||||
}
|
||||
bodyStr, err := coerceGojaBody(call.Argument(1))
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
headers := parseGojaHeaders(call.Argument(2).Export())
|
||||
// POST always sends a (possibly empty) body and defaults Content-Type.
|
||||
return r.doExtensionHTTP("POST", urlStr, strings.NewReader(bodyStr), true, headers)
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpRequest(call goja.FunctionCall) goja.Value {
|
||||
urlStr, errVal := r.checkExtensionURL(call)
|
||||
if errVal != nil {
|
||||
return errVal
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
var bodyStr string
|
||||
headers := make(map[string]string)
|
||||
var headers map[string]string
|
||||
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
optionsObj := call.Arguments[1].Export()
|
||||
if opts, ok := optionsObj.(map[string]any); ok {
|
||||
if m, ok := opts["method"].(string); ok {
|
||||
method = strings.ToUpper(m)
|
||||
}
|
||||
|
||||
if bodyArg, ok := opts["body"]; ok && bodyArg != nil {
|
||||
switch v := bodyArg.(type) {
|
||||
case string:
|
||||
bodyStr = v
|
||||
case map[string]any, []any:
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": fmt.Sprintf("failed to stringify body: %v", err),
|
||||
})
|
||||
}
|
||||
bodyStr = string(jsonBytes)
|
||||
default:
|
||||
bodyStr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
if h, ok := opts["headers"].(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
if opts, ok := call.Argument(1).Export().(map[string]any); ok {
|
||||
if m, ok := opts["method"].(string); ok {
|
||||
method = strings.ToUpper(m)
|
||||
}
|
||||
if bodyArg, ok := opts["body"]; ok && bodyArg != nil {
|
||||
var err error
|
||||
if bodyStr, err = coerceExportedBody(bodyArg); err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
headers = parseGojaHeaders(opts["headers"])
|
||||
}
|
||||
|
||||
var reqBody io.Reader
|
||||
if bodyStr != "" {
|
||||
reqBody = strings.NewReader(bodyStr)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, urlStr, reqBody)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
req = r.bindDownloadCancelContext(req)
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
setDefaultExtensionUA(req)
|
||||
if bodyStr != "" && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
respHeaders := make(map[string]any)
|
||||
for k, v := range resp.Header {
|
||||
if len(v) == 1 {
|
||||
respHeaders[k] = v[0]
|
||||
} else {
|
||||
respHeaders[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"statusCode": resp.StatusCode,
|
||||
"status": resp.StatusCode,
|
||||
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||
"url": resp.Request.URL.String(),
|
||||
"body": string(body),
|
||||
"headers": respHeaders,
|
||||
})
|
||||
return r.doExtensionHTTP(method, urlStr, reqBody, bodyStr != "", headers)
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpPut(call goja.FunctionCall) goja.Value {
|
||||
@@ -365,115 +264,30 @@ func (r *extensionRuntime) httpPatch(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpMethodShortcut(method string, call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": "URL is required",
|
||||
})
|
||||
}
|
||||
|
||||
urlStr := call.Arguments[0].String()
|
||||
|
||||
if err := r.validateDomain(urlStr); err != nil {
|
||||
GoLog("[Extension:%s] HTTP blocked: %v\n", r.extensionID, err)
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
urlStr, errVal := r.checkExtensionURL(call)
|
||||
if errVal != nil {
|
||||
return errVal
|
||||
}
|
||||
|
||||
// DELETE takes (url, headers); other methods take (url, body, headers).
|
||||
var bodyStr string
|
||||
headers := make(map[string]string)
|
||||
|
||||
if method == "DELETE" {
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
headersObj := call.Arguments[1].Export()
|
||||
if h, ok := headersObj.(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
bodyArg := call.Arguments[1].Export()
|
||||
switch v := bodyArg.(type) {
|
||||
case string:
|
||||
bodyStr = v
|
||||
case map[string]any, []any:
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": fmt.Sprintf("failed to stringify body: %v", err),
|
||||
})
|
||||
}
|
||||
bodyStr = string(jsonBytes)
|
||||
default:
|
||||
bodyStr = call.Arguments[1].String()
|
||||
}
|
||||
}
|
||||
|
||||
if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) {
|
||||
headersObj := call.Arguments[2].Export()
|
||||
if h, ok := headersObj.(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
headerArg := 1
|
||||
if method != "DELETE" {
|
||||
var err error
|
||||
if bodyStr, err = coerceGojaBody(call.Argument(1)); err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
headerArg = 2
|
||||
}
|
||||
headers := parseGojaHeaders(call.Argument(headerArg).Export())
|
||||
|
||||
var reqBody io.Reader
|
||||
if bodyStr != "" {
|
||||
reqBody = strings.NewReader(bodyStr)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, urlStr, reqBody)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
req = r.bindDownloadCancelContext(req)
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
setDefaultExtensionUA(req)
|
||||
if bodyStr != "" && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
respHeaders := make(map[string]any)
|
||||
for k, v := range resp.Header {
|
||||
if len(v) == 1 {
|
||||
respHeaders[k] = v[0]
|
||||
} else {
|
||||
respHeaders[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"statusCode": resp.StatusCode,
|
||||
"status": resp.StatusCode,
|
||||
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||
"url": resp.Request.URL.String(),
|
||||
"body": string(body),
|
||||
"headers": respHeaders,
|
||||
})
|
||||
return r.doExtensionHTTP(method, urlStr, reqBody, bodyStr != "", headers)
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) httpClearCookies(call goja.FunctionCall) goja.Value {
|
||||
|
||||
@@ -25,39 +25,19 @@ func (r *extensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value {
|
||||
|
||||
method := "GET"
|
||||
var bodyStr string
|
||||
headers := make(map[string]string)
|
||||
var headers map[string]string
|
||||
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
optionsObj := call.Arguments[1].Export()
|
||||
if opts, ok := optionsObj.(map[string]any); ok {
|
||||
if m, ok := opts["method"].(string); ok {
|
||||
method = strings.ToUpper(m)
|
||||
}
|
||||
|
||||
if bodyArg, ok := opts["body"]; ok && bodyArg != nil {
|
||||
switch v := bodyArg.(type) {
|
||||
case string:
|
||||
bodyStr = v
|
||||
case map[string]any, []any:
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return r.createFetchError(fmt.Sprintf("failed to stringify body: %v", err))
|
||||
}
|
||||
bodyStr = string(jsonBytes)
|
||||
default:
|
||||
bodyStr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
if h, ok := opts["headers"]; ok && h != nil {
|
||||
switch hv := h.(type) {
|
||||
case map[string]any:
|
||||
for k, v := range hv {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
if opts, ok := call.Argument(1).Export().(map[string]any); ok {
|
||||
if m, ok := opts["method"].(string); ok {
|
||||
method = strings.ToUpper(m)
|
||||
}
|
||||
if bodyArg, ok := opts["body"]; ok && bodyArg != nil {
|
||||
var err error
|
||||
if bodyStr, err = coerceExportedBody(bodyArg); err != nil {
|
||||
return r.createFetchError(err.Error())
|
||||
}
|
||||
}
|
||||
headers = parseGojaHeaders(opts["headers"])
|
||||
}
|
||||
|
||||
var reqBody io.Reader
|
||||
@@ -92,14 +72,7 @@ func (r *extensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value {
|
||||
return r.createFetchError(err.Error())
|
||||
}
|
||||
|
||||
respHeaders := make(map[string]any)
|
||||
for k, v := range resp.Header {
|
||||
if len(v) == 1 {
|
||||
respHeaders[k] = v[0]
|
||||
} else {
|
||||
respHeaders[k] = v
|
||||
}
|
||||
}
|
||||
respHeaders := flattenHTTPHeaders(resp.Header)
|
||||
|
||||
responseObj := r.vm.NewObject()
|
||||
responseObj.Set("ok", resp.StatusCode >= 200 && resp.StatusCode < 300)
|
||||
|
||||
@@ -481,9 +481,6 @@ func TestExtensionStoreSettingsAndRuntimeStorage(t *testing.T) {
|
||||
if cats := store.getCategories(); len(cats) != 5 {
|
||||
t.Fatalf("categories = %#v", cats)
|
||||
}
|
||||
if !containsIgnoreCase("Hello Metadata", "metadata") || findSubstring("abcdef", "cd") != 2 || containsStr("abc", "z") {
|
||||
t.Fatal("string helper mismatch")
|
||||
}
|
||||
if err := requireHTTPSURL("http://example.com", "registry"); err == nil {
|
||||
t.Fatal("expected HTTPS validation error")
|
||||
}
|
||||
|
||||
@@ -335,14 +335,7 @@ func (r *extensionRuntime) signedSessionFetch(call goja.FunctionCall) goja.Value
|
||||
body = []byte(call.Arguments[2].String())
|
||||
}
|
||||
}
|
||||
extraHeaders := map[string]string{}
|
||||
if len(call.Arguments) > 3 && !goja.IsUndefined(call.Arguments[3]) && !goja.IsNull(call.Arguments[3]) {
|
||||
if h, ok := call.Arguments[3].Export().(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
extraHeaders[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
extraHeaders := parseGojaHeaders(call.Argument(3).Export())
|
||||
|
||||
record, err := r.ensureSignedSession(config)
|
||||
if err != nil {
|
||||
|
||||
@@ -511,7 +511,7 @@ func (s *extensionStore) searchExtensions(query string, category string) ([]stor
|
||||
}
|
||||
|
||||
result := make([]storeExtensionResponse, 0, len(extensions))
|
||||
queryLower := toLower(query)
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
for _, ext := range extensions {
|
||||
if category != "" && ext.Category != category {
|
||||
@@ -519,12 +519,12 @@ func (s *extensionStore) searchExtensions(query string, category string) ([]stor
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
if !containsIgnoreCase(ext.Name, queryLower) &&
|
||||
!containsIgnoreCase(ext.DisplayName, queryLower) &&
|
||||
!containsIgnoreCase(ext.Description, queryLower) {
|
||||
if !strings.Contains(strings.ToLower(ext.Name), queryLower) &&
|
||||
!strings.Contains(strings.ToLower(ext.DisplayName), queryLower) &&
|
||||
!strings.Contains(strings.ToLower(ext.Description), queryLower) {
|
||||
found := false
|
||||
for _, tag := range ext.Tags {
|
||||
if containsIgnoreCase(tag, queryLower) {
|
||||
if strings.Contains(strings.ToLower(tag), queryLower) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -555,32 +555,3 @@ func (s *extensionStore) clearCache() {
|
||||
|
||||
LogInfo("ExtensionStore", "Cache cleared")
|
||||
}
|
||||
|
||||
func containsIgnoreCase(s, substr string) bool {
|
||||
return containsStr(toLower(s), substr)
|
||||
}
|
||||
|
||||
func toLower(s string) string {
|
||||
result := make([]byte, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
c += 'a' - 'A'
|
||||
}
|
||||
result[i] = c
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func containsStr(s, substr string) bool {
|
||||
return len(substr) == 0 || (len(s) >= len(substr) && findSubstring(s, substr) >= 0)
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) int {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
+23
-37
@@ -429,68 +429,54 @@ func NewLyricsClient() *LyricsClient {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsResponse, error) {
|
||||
baseURL := "https://lrclib.net/api/get"
|
||||
params := url.Values{}
|
||||
params.Set("artist_name", artist)
|
||||
params.Set("track_name", track)
|
||||
|
||||
fullURL := baseURL + "?" + params.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
// lrclibGet performs a GET against lrclib.net and decodes the JSON body into
|
||||
// dst. 404 is reported as a typed lyrics-not-found error.
|
||||
func (c *LyricsClient) lrclibGet(path string, params url.Values, dst any) error {
|
||||
req, err := http.NewRequest("GET", "https://lrclib.net"+path+"?"+params.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", getRandomUserAgent())
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch lyrics: %w", err)
|
||||
return fmt.Errorf("failed to fetch lyrics: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, lyricsNotFoundErrorf("lyrics not found")
|
||||
return lyricsNotFoundErrorf("lyrics not found")
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return lyricsHTTPStatusError(resp.StatusCode, "unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, lyricsHTTPStatusError(resp.StatusCode, "unexpected status code: %d", resp.StatusCode)
|
||||
if err := json.NewDecoder(resp.Body).Decode(dst); err != nil {
|
||||
return fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsResponse, error) {
|
||||
params := url.Values{}
|
||||
params.Set("artist_name", artist)
|
||||
params.Set("track_name", track)
|
||||
|
||||
var lrcResp LRCLibResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&lrcResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
if err := c.lrclibGet("/api/get", params, &lrcResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.parseLRCLibResponse(&lrcResp), nil
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec float64) (*LyricsResponse, error) {
|
||||
baseURL := "https://lrclib.net/api/search"
|
||||
params := url.Values{}
|
||||
params.Set("q", query)
|
||||
|
||||
fullURL := baseURL + "?" + params.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", getRandomUserAgent())
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search lyrics: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, lyricsHTTPStatusError(resp.StatusCode, "unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var results []LRCLibResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
if err := c.lrclibGet("/api/search", params, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user