refactor(backend): dedupe response builders and MusicBrainz fetch

marshalJSONString collapses 62 identical marshal-and-return blocks in the
gomobile exports; jsError/jsSuccess collapse 135 goja extension-response
maps; the two MusicBrainz fetchers share one request+retry helper; the
extension-HTTP default User-Agent block and fileExists get single homes.
No exported signature changes.
This commit is contained in:
zarzet
2026-07-11 16:34:36 +07:00
parent 9580fafe4f
commit 8c2d2d5289
18 changed files with 286 additions and 871 deletions
+2 -12
View File
@@ -12,12 +12,7 @@ func CheckAvailability(spotifyID, isrc string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(availability)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(availability)
}
// SetSongLinkNetworkOptions is kept for backward compatibility.
@@ -44,12 +39,7 @@ func CheckDuplicate(outputDir, isrc string) (string, error) {
"filepath": existingFile,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func CheckDuplicatesBatch(outputDir, tracksJSON string) (string, error) {
+10 -49
View File
@@ -39,8 +39,8 @@ func PreWarmTrackCacheJSON(tracksJSON string) (string, error) {
"message": fmt.Sprintf("Pre-warming cache for %d tracks in background", len(tracks)),
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
func GetTrackCacheSize() int {
@@ -64,11 +64,7 @@ func GetDeezerRelatedArtists(artistID string, limit int) (string, error) {
resp := map[string]any{
"artists": artists,
}
jsonBytes, err := json.Marshal(resp)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(resp)
}
func GetDeezerMetadata(resourceType, resourceID string) (string, error) {
@@ -96,12 +92,7 @@ func GetDeezerMetadata(resourceType, resourceID string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(data)
}
func GetDeezerExtendedMetadata(trackID string) (string, error) {
@@ -121,12 +112,7 @@ func GetDeezerExtendedMetadata(trackID string) (string, error) {
result := buildDeezerExtendedMetadataResult(metadata)
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func SearchDeezerByISRC(isrc string) (string, error) {
@@ -159,12 +145,7 @@ func SearchDeezerByISRCForItemID(isrc string, itemID string) (string, error) {
}
result := buildDeezerISRCSearchResult(track)
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func buildDeezerExtendedMetadataResult(metadata *AlbumExtendedMetadata) map[string]string {
@@ -236,12 +217,7 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) {
return "", fmt.Errorf("failed to fetch Deezer metadata: %w", err)
}
jsonBytes, err := json.Marshal(trackResp)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(trackResp)
}
if resourceType == "album" {
@@ -255,12 +231,7 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) {
return "", fmt.Errorf("failed to fetch Deezer album metadata: %w", err)
}
jsonBytes, err := json.Marshal(albumResp)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(albumResp)
}
return "", fmt.Errorf("spotify to Deezer conversion only supported for tracks and albums: please search by name for %s", resourceType)
@@ -273,12 +244,7 @@ func CheckAvailabilityFromDeezerID(deezerTrackID string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(availability)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(availability)
}
func CheckAvailabilityByPlatformID(platform, entityType, entityID string) (string, error) {
@@ -288,12 +254,7 @@ func CheckAvailabilityByPlatformID(platform, entityType, entityID string) (strin
return "", err
}
jsonBytes, err := json.Marshal(availability)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(availability)
}
func GetSpotifyIDFromDeezerTrack(deezerTrackID string) (string, error) {
+2 -2
View File
@@ -438,8 +438,8 @@ func errorResponse(msg string) (string, error) {
Error: msg,
ErrorType: errorType,
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
func classifyDownloadErrorType(msg string) string {
+24 -139
View File
@@ -248,11 +248,7 @@ func GetProviderMetadataJSON(providerID, resourceType, resourceID string) (strin
if err != nil {
return "", err
}
jsonBytes, err := json.Marshal(response)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(response)
}
return GetDeezerMetadata(resourceType, resourceID)
default:
@@ -261,11 +257,7 @@ func GetProviderMetadataJSON(providerID, resourceType, resourceID string) (strin
return "", err
}
jsonBytes, err := json.Marshal(response)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(response)
}
}
@@ -309,12 +301,7 @@ func LoadExtensionsFromDir(dirPath string) (string, error) {
result["errors"].([]string)[i] = err.Error()
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func LoadExtensionFromPath(filePath string) (string, error) {
@@ -332,12 +319,7 @@ func LoadExtensionFromPath(filePath string) (string, error) {
"enabled": ext.Enabled,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func UnloadExtensionByID(extensionID string) error {
@@ -364,12 +346,7 @@ func UpgradeExtensionFromPath(filePath string) (string, error) {
"enabled": ext.Enabled,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func CheckExtensionUpgradeFromPath(filePath string) (string, error) {
@@ -399,11 +376,7 @@ func SetProviderPriorityJSON(priorityJSON string) error {
func GetProviderPriorityJSON() (string, error) {
priority := GetProviderPriority()
jsonBytes, err := json.Marshal(priority)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(priority)
}
func SetExtensionFallbackProviderIDsJSON(providerIDsJSON string) error {
@@ -423,11 +396,7 @@ func SetExtensionFallbackProviderIDsJSON(providerIDsJSON string) error {
func GetExtensionFallbackProviderIDsJSON() (string, error) {
providerIDs := GetExtensionFallbackProviderIDs()
jsonBytes, err := json.Marshal(providerIDs)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(providerIDs)
}
func SetMetadataProviderPriorityJSON(priorityJSON string) error {
@@ -442,23 +411,14 @@ func SetMetadataProviderPriorityJSON(priorityJSON string) error {
func GetMetadataProviderPriorityJSON() (string, error) {
priority := GetMetadataProviderPriority()
jsonBytes, err := json.Marshal(priority)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(priority)
}
func GetExtensionSettingsJSON(extensionID string) (string, error) {
store := GetExtensionSettingsStore()
settings := store.GetAll(extensionID)
jsonBytes, err := json.Marshal(settings)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(settings)
}
func SetExtensionSettingsJSON(extensionID, settingsJSON string) error {
@@ -483,12 +443,7 @@ func SearchTracksWithExtensionsJSON(query string, limit int) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(tracks)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(tracks)
}
func SearchTracksWithMetadataProvidersJSON(query string, limit int, includeExtensions bool) (string, error) {
@@ -498,12 +453,7 @@ func SearchTracksWithMetadataProvidersJSON(query string, limit int, includeExten
return "", err
}
jsonBytes, err := json.Marshal(tracks)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(tracks)
}
func DownloadWithExtensionsJSON(requestJSON string) (string, error) {
@@ -542,12 +492,7 @@ func DownloadWithExtensionsJSON(requestJSON string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func CleanupExtensions() {
@@ -562,12 +507,7 @@ func InvokeExtensionActionJSON(extensionID, actionName string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func GetExtensionPendingAuthJSON(extensionID string) (string, error) {
@@ -582,12 +522,7 @@ func GetExtensionPendingAuthJSON(extensionID string) (string, error) {
"callback_url": req.CallbackURL,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func ensureExtensionPendingAuthRequest(extensionID string) *PendingAuthRequest {
@@ -679,12 +614,7 @@ func GetAllPendingAuthRequestsJSON() (string, error) {
})
}
jsonBytes, err := json.Marshal(requests)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(requests)
}
func GetPendingFFmpegCommandJSON(commandID string) (string, error) {
@@ -701,12 +631,7 @@ func GetPendingFFmpegCommandJSON(commandID string) (string, error) {
"output_path": cmd.OutputPath,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func SetFFmpegCommandResultByID(commandID string, success bool, output, errorMsg string) {
@@ -728,12 +653,7 @@ func GetAllPendingFFmpegCommandsJSON() (string, error) {
}
}
jsonBytes, err := json.Marshal(commands)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(commands)
}
func EnrichTrackWithExtensionJSON(extensionID, trackJSON string) (string, error) {
@@ -820,12 +740,7 @@ func CustomSearchWithExtensionJSONWithRequestID(extensionID, query string, optio
}
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func GetSearchProvidersJSON() (string, error) {
@@ -843,12 +758,7 @@ func GetSearchProvidersJSON() (string, error) {
})
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func HandleURLWithExtensionJSON(url string) (string, error) {
@@ -1022,12 +932,7 @@ func HandleURLWithExtensionJSON(url string) (string, error) {
response["artist"] = artistResponse
}
jsonBytes, err := json.Marshal(response)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(response)
}
func FindURLHandlerJSON(url string) string {
@@ -1052,12 +957,7 @@ func GetURLHandlersJSON() (string, error) {
})
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func RunPostProcessingJSON(filePath, metadataJSON string) (string, error) {
@@ -1074,12 +974,7 @@ func RunPostProcessingJSON(filePath, metadataJSON string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func RunPostProcessingV2JSON(inputJSON, metadataJSON string) (string, error) {
@@ -1103,12 +998,7 @@ func RunPostProcessingV2JSON(inputJSON, metadataJSON string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func GetPostProcessingProvidersJSON() (string, error) {
@@ -1135,12 +1025,7 @@ func GetPostProcessingProvidersJSON() (string, error) {
})
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func callExtensionFunctionJSON(extensionID, functionName string, timeout time.Duration) (string, error) {
+9 -39
View File
@@ -23,12 +23,7 @@ func FetchLyrics(spotifyID, trackName, artistName string, durationMs int64) (str
"instrumental": lyrics.Instrumental,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string, durationMs int64) (string, error) {
@@ -69,11 +64,7 @@ func GetLyricsLRCWithSource(spotifyID, trackName, artistName string, filePath st
"sync_type": "EMBEDDED",
"instrumental": false,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
result := map[string]any{
@@ -82,11 +73,7 @@ func GetLyricsLRCWithSource(spotifyID, trackName, artistName string, filePath st
"sync_type": "",
"instrumental": false,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
client := NewLyricsClient()
@@ -109,12 +96,7 @@ func GetLyricsLRCWithSource(spotifyID, trackName, artistName string, filePath st
"sync_type": lyricsData.SyncType,
"instrumental": lyricsData.Instrumental,
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
func EmbedLyricsToFile(filePath, lyrics string) (string, error) {
@@ -128,8 +110,8 @@ func EmbedLyricsToFile(filePath, lyrics string) (string, error) {
"message": "Lyrics embedded successfully",
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
func FetchAndSaveLyrics(trackName, artistName, spotifyID string, durationMs int64, outputPath string, audioFilePath string) error {
@@ -183,20 +165,12 @@ func SetLyricsProvidersJSON(providersJSON string) error {
func GetLyricsProvidersJSON() (string, error) {
providers := GetLyricsProviderOrder()
jsonBytes, err := json.Marshal(providers)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(providers)
}
func GetAvailableLyricsProvidersJSON() (string, error) {
providers := GetAvailableLyricsProviders()
jsonBytes, err := json.Marshal(providers)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(providers)
}
func SetLyricsFetchOptionsJSON(optionsJSON string) error {
@@ -213,9 +187,5 @@ func SetLyricsFetchOptionsJSON(optionsJSON string) error {
func GetLyricsFetchOptionsJSON() (string, error) {
opts := GetLyricsFetchOptions()
jsonBytes, err := json.Marshal(opts)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(opts)
}
+25 -30
View File
@@ -314,12 +314,7 @@ func ReadFileMetadata(filePath string) (string, error) {
return "", fmt.Errorf("unsupported file format: %s", filePath)
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(result)
}
// ParseCueSheet is called from Dart to get track listing and timing data for CUE splitting.
@@ -382,8 +377,8 @@ func WriteM4AFreeformTags(filePath, metadataJSON string) (string, error) {
}
resp := map[string]any{"success": true, "method": "native_m4a_freeform"}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
// EnsureAC4Config normalizes a decrypted AC-4 file to a standards-compliant ISO
@@ -405,8 +400,8 @@ func WriteAC4Metadata(filePath, metadataJSON, coverPath string) (string, error)
return "", fmt.Errorf("failed to write AC-4 metadata: %w", err)
}
resp := map[string]any{"success": true, "handled": handled}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
// EditFileMetadata writes audio file tags: FLAC via native Go library, MP3/Opus returns map for Dart/FFmpeg.
@@ -433,8 +428,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
"success": true,
"method": "native_m4a_replaygain",
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
if isFlac {
@@ -455,8 +450,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
"success": true,
"method": "native",
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
// WAV / AIFF: write tags into an embedded ID3v2.4 chunk natively.
@@ -465,16 +460,16 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
return "", fmt.Errorf("failed to write WAV metadata: %w", err)
}
resp := map[string]any{"success": true, "method": "native_wav"}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
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"}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
if isApeFile {
@@ -569,8 +564,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
"success": true,
"method": "native_ape",
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
// MP3, Ogg/Opus, and M4A have native editors that preserve foreign
@@ -584,8 +579,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
GoLog("[Metadata] Native MP3 edit failed, falling back to ffmpeg: %v\n", err)
} else {
resp := map[string]any{"success": true, "method": "native_mp3"}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
}
if isOggFile {
@@ -593,8 +588,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
GoLog("[Metadata] Native Ogg edit failed, falling back to ffmpeg: %v\n", err)
} else {
resp := map[string]any{"success": true, "method": "native_ogg"}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
}
if isM4AFile || isMP4ContainerFile(filePath) {
@@ -602,8 +597,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
GoLog("[Metadata] Native M4A edit failed, falling back to ffmpeg: %v\n", err)
} else {
resp := map[string]any{"success": true, "method": "native_m4a"}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
}
@@ -612,8 +607,8 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
"method": "ffmpeg",
"fields": fields,
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
func isMP4ContainerFile(filePath string) bool {
@@ -669,8 +664,8 @@ func RewriteSplitArtistTagsExport(filePath, artist, albumArtist string) (string,
"message": "Split artist tags written successfully",
}
jsonBytes, _ := json.Marshal(resp)
return string(jsonBytes), nil
s, _ := marshalJSONString(resp)
return s, nil
}
func DownloadCoverToFile(coverURL string, outputPath string, maxQuality bool) error {
+17 -50
View File
@@ -112,7 +112,10 @@ func selectMusicBrainzAlbumArtist(releases []musicBrainzRelease, albumName strin
return ""
}
func FetchMusicBrainzAlbumArtistByISRC(isrc string, albumName string) (string, error) {
// fetchMusicBrainzRecordingByISRC queries the MusicBrainz recording endpoint
// for the given ISRC with the supplied inc= parameter, retrying up to 3 times,
// and decodes the JSON response into payload. It returns the normalized ISRC.
func fetchMusicBrainzRecordingByISRC(isrc string, inc string, payload any) (string, error) {
normalizedISRC := strings.ToUpper(strings.TrimSpace(isrc))
if normalizedISRC == "" {
return "", fmt.Errorf("no ISRC provided")
@@ -121,9 +124,10 @@ func FetchMusicBrainzAlbumArtistByISRC(isrc string, albumName string) (string, e
client := NewMetadataHTTPClient(10 * time.Second)
query := fmt.Sprintf("isrc:%s", normalizedISRC)
reqURL := fmt.Sprintf(
"%s/recording?query=%s&fmt=json&inc=releases+artist-credits",
"%s/recording?query=%s&fmt=json&inc=%s",
musicBrainzAPIBase,
url.QueryEscape(query),
inc,
)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
@@ -159,8 +163,16 @@ func FetchMusicBrainzAlbumArtistByISRC(isrc string, albumName string) (string, e
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(payload); err != nil {
return "", err
}
return normalizedISRC, nil
}
func FetchMusicBrainzAlbumArtistByISRC(isrc string, albumName string) (string, error) {
var payload musicBrainzAlbumArtistResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
normalizedISRC, err := fetchMusicBrainzRecordingByISRC(isrc, "releases+artist-credits", &payload)
if err != nil {
return "", err
}
for _, recording := range payload.Recordings {
@@ -173,54 +185,9 @@ func FetchMusicBrainzAlbumArtistByISRC(isrc string, albumName string) (string, e
}
func FetchMusicBrainzGenreByISRC(isrc string) (string, error) {
normalizedISRC := strings.ToUpper(strings.TrimSpace(isrc))
if normalizedISRC == "" {
return "", fmt.Errorf("no ISRC provided")
}
client := NewMetadataHTTPClient(10 * time.Second)
query := fmt.Sprintf("isrc:%s", normalizedISRC)
reqURL := fmt.Sprintf(
"%s/recording?query=%s&fmt=json&inc=tags",
musicBrainzAPIBase,
url.QueryEscape(query),
)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", getRandomUserAgent())
var resp *http.Response
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
resp, lastErr = client.Do(req)
if lastErr == nil && resp.StatusCode == http.StatusOK {
break
}
if resp != nil {
resp.Body.Close()
}
if attempt < 2 {
time.Sleep(2 * time.Second)
}
}
if lastErr != nil {
return "", lastErr
}
if resp == nil {
return "", fmt.Errorf("MusicBrainz request failed without response")
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return "", fmt.Errorf("MusicBrainz API returned status: %d", resp.StatusCode)
}
defer resp.Body.Close()
var payload musicBrainzRecordingResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
normalizedISRC, err := fetchMusicBrainzRecordingByISRC(isrc, "tags", &payload)
if err != nil {
return "", err
}
if len(payload.Recordings) == 0 {
+4 -4
View File
@@ -739,8 +739,8 @@ func ReEnrichFile(requestJSON string) (string, error) {
req.lyricsSidecarEnabled() &&
strings.TrimSpace(lyricsLRC) != "",
}
jsonBytes, _ := json.Marshal(result)
return string(jsonBytes), nil
s, _ := marshalJSONString(result)
return s, nil
}
// Don't cleanup cover temp — Dart needs it for FFmpeg embed
@@ -759,6 +759,6 @@ func ReEnrichFile(requestJSON string) (string, error) {
strings.TrimSpace(lyricsLRC) != "",
}
jsonBytes, _ := json.Marshal(result)
return string(jsonBytes), nil
s, _ := marshalJSONString(result)
return s, nil
}
+3 -19
View File
@@ -1,7 +1,6 @@
package gobackend
import (
"encoding/json"
"fmt"
"net/url"
"path/filepath"
@@ -63,12 +62,7 @@ func GetStoreExtensionsJSON(forceRefresh bool) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(extensions)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(extensions)
}
func SearchStoreExtensionsJSON(query, category string) (string, error) {
@@ -82,12 +76,7 @@ func SearchStoreExtensionsJSON(query, category string) (string, error) {
return "", err
}
jsonBytes, err := json.Marshal(extensions)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(extensions)
}
func GetStoreCategoriesJSON() (string, error) {
@@ -97,12 +86,7 @@ func GetStoreCategoriesJSON() (string, error) {
}
categories := store.getCategories()
jsonBytes, err := json.Marshal(categories)
if err != nil {
return "", err
}
return string(jsonBytes), nil
return marshalJSONString(categories)
}
func storeExtensionPackageSuffix(downloadURL string) string {
+18 -68
View File
@@ -54,10 +54,7 @@ func summarizeURLForLog(urlStr string) string {
func (r *extensionRuntime) authOpenUrl(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "auth URL is required",
})
return r.jsError("auth URL is required")
}
authURL := call.Arguments[0].String()
@@ -67,10 +64,7 @@ func (r *extensionRuntime) authOpenUrl(call goja.FunctionCall) goja.Value {
}
if err := validateExtensionAuthURL(authURL); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
pendingAuthRequestsMu.Lock()
@@ -94,8 +88,7 @@ func (r *extensionRuntime) authOpenUrl(call goja.FunctionCall) goja.Value {
GoLog("[Extension:%s] Auth URL requested: %s\n", r.extensionID, summarizeURLForLog(authURL))
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"message": "Auth URL will be opened by the app",
})
}
@@ -284,19 +277,13 @@ func (r *extensionRuntime) authGetPKCE(call goja.FunctionCall) goja.Value {
func (r *extensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "config object is required",
})
return r.jsError("config object is required")
}
configObj := call.Arguments[0].Export()
config, ok := configObj.(map[string]any)
if !ok {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "config must be an object",
})
return r.jsError("config must be an object")
}
authURL, _ := config["authUrl"].(string)
@@ -304,16 +291,10 @@ func (r *extensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V
redirectURI, _ := config["redirectUri"].(string)
if authURL == "" || clientID == "" || redirectURI == "" {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "authUrl, clientId, and redirectUri are required",
})
return r.jsError("authUrl, clientId, and redirectUri are required")
}
if err := validateExtensionAuthURL(authURL); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
scope, _ := config["scope"].(string)
@@ -321,10 +302,7 @@ func (r *extensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V
verifier, err := generatePKCEVerifier(64)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to generate PKCE: %v", err),
})
return r.jsError("failed to generate PKCE: %v", err)
}
challenge := generatePKCEChallenge(verifier)
@@ -341,10 +319,7 @@ func (r *extensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V
parsedURL, err := url.Parse(authURL)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("invalid authUrl: %v", err),
})
return r.jsError("invalid authUrl: %v", err)
}
query := parsedURL.Query()
@@ -376,8 +351,7 @@ func (r *extensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V
GoLog("[Extension:%s] PKCE OAuth started: %s\n", r.extensionID, summarizeURLForLog(fullAuthURL))
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"authUrl": fullAuthURL,
"pkce": map[string]any{
"verifier": verifier,
@@ -389,19 +363,13 @@ func (r *extensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V
func (r *extensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "config object is required",
})
return r.jsError("config object is required")
}
configObj := call.Arguments[0].Export()
config, ok := configObj.(map[string]any)
if !ok {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "config must be an object",
})
return r.jsError("config must be an object")
}
tokenURL, _ := config["tokenUrl"].(string)
@@ -410,10 +378,7 @@ func (r *extensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja
code, _ := config["code"].(string)
if tokenURL == "" || clientID == "" || code == "" {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "tokenUrl, clientId, and code are required",
})
return r.jsError("tokenUrl, clientId, and code are required")
}
extensionAuthStateMu.RLock()
@@ -425,17 +390,11 @@ func (r *extensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja
extensionAuthStateMu.RUnlock()
if verifier == "" {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "no PKCE verifier found - call generatePKCE or startOAuthWithPKCE first",
})
return r.jsError("no PKCE verifier found - call generatePKCE or startOAuthWithPKCE first")
}
if err := r.validateDomain(tokenURL); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
formData := url.Values{}
@@ -455,10 +414,7 @@ func (r *extensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(formData.Encode()))
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
req = r.bindDownloadCancelContext(req)
@@ -467,19 +423,13 @@ func (r *extensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja
resp, err := r.httpClient.Do(req)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
bodyPreview := sanitizeSensitiveLogText(string(body))
if len(bodyPreview) > 1000 {
+13 -46
View File
@@ -270,43 +270,28 @@ func removePKCS7Padding(data []byte, blockSize int) ([]byte, error) {
func (r *extensionRuntime) transformBlockCipher(call goja.FunctionCall, decrypt bool) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "data and options are required",
})
return r.jsError("data and options are required")
}
options := parseRuntimeOptionsArgument(call, 1)
parsedOptions, err := parseRuntimeBlockCipherOptions(options)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
switch parsedOptions.Mode {
case "cbc", "ctr":
default:
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("unsupported block cipher mode: %s", parsedOptions.Mode),
})
return r.jsError("unsupported block cipher mode: %s", parsedOptions.Mode)
}
inputData, err := decodeRuntimeBytesValue(call.Arguments[0].Export(), parsedOptions.InputEncoding)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
block, err := newRuntimeBlockCipher(parsedOptions)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
if len(parsedOptions.IV) != block.BlockSize() {
@@ -314,10 +299,7 @@ func (r *extensionRuntime) transformBlockCipher(call goja.FunctionCall, decrypt
if parsedOptions.Mode == "ctr" {
ivLabel = "iv (counter)"
}
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("%s must be %d bytes for %s", ivLabel, block.BlockSize(), parsedOptions.Algorithm),
})
return r.jsError("%s must be %d bytes for %s", ivLabel, block.BlockSize(), parsedOptions.Algorithm)
}
var output []byte
@@ -332,10 +314,7 @@ func (r *extensionRuntime) transformBlockCipher(call goja.FunctionCall, decrypt
data = applyPKCS7Padding(data, block.BlockSize())
}
if len(data)%block.BlockSize() != 0 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("input length must be a multiple of %d bytes", block.BlockSize()),
})
return r.jsError("input length must be a multiple of %d bytes", block.BlockSize())
}
output = make([]byte, len(data))
@@ -344,10 +323,7 @@ func (r *extensionRuntime) transformBlockCipher(call goja.FunctionCall, decrypt
if parsedOptions.Padding == "pkcs7" {
output, err = removePKCS7Padding(output, block.BlockSize())
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
}
} else {
@@ -357,14 +333,10 @@ func (r *extensionRuntime) transformBlockCipher(call goja.FunctionCall, decrypt
encoded, err := encodeRuntimeBytes(output, parsedOptions.OutputEncoding)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"data": encoded,
"block_size": block.BlockSize(),
})
@@ -405,10 +377,7 @@ func (r *extensionRuntime) decryptBlockCipher(call goja.FunctionCall) goja.Value
// Returns { success, data, segments_processed } or { success:false, error }.
func (r *extensionRuntime) decryptCTRSegments(call goja.FunctionCall) goja.Value {
fail := func(msg string) goja.Value {
return r.vm.ToValue(map[string]any{
"success": false,
"error": msg,
})
return r.jsError("%s", msg)
}
if len(call.Arguments) < 2 {
@@ -516,8 +485,7 @@ func (r *extensionRuntime) decryptCTRSegments(call goja.FunctionCall) goja.Value
// Return raw bytes as an ArrayBuffer when requested (zero-copy-ish, no
// base64). Otherwise fall back to an encoded string.
if outputEncoding == "bytes" || outputEncoding == "raw" {
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"data": r.vm.NewArrayBuffer(data),
"segments_processed": processed,
})
@@ -528,8 +496,7 @@ func (r *extensionRuntime) decryptCTRSegments(call goja.FunctionCall) goja.Value
return fail(err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"data": encoded,
"segments_processed": processed,
})
+6 -22
View File
@@ -52,10 +52,7 @@ func ClearFFmpegCommand(commandID string) {
func (r *extensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "command is required",
})
return r.jsError("command is required")
}
command := call.Arguments[0].String()
@@ -97,10 +94,7 @@ func (r *extensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value {
if time.Since(start) > timeout {
ClearFFmpegCommand(cmdID)
return r.vm.ToValue(map[string]any{
"success": false,
"error": "FFmpeg command timed out",
})
return r.jsError("FFmpeg command timed out")
}
time.Sleep(100 * time.Millisecond)
@@ -109,24 +103,17 @@ func (r *extensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value {
func (r *extensionRuntime) ffmpegGetInfo(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "file path is required",
})
return r.jsError("file path is required")
}
filePath := call.Arguments[0].String()
quality, err := GetAudioQuality(filePath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"bit_depth": quality.BitDepth,
"sample_rate": quality.SampleRate,
"total_samples": quality.TotalSamples,
@@ -137,10 +124,7 @@ func (r *extensionRuntime) ffmpegGetInfo(call goja.FunctionCall) goja.Value {
func (r *extensionRuntime) ffmpegConvert(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "input and output paths are required",
})
return r.jsError("input and output paths are required")
}
inputPath := call.Arguments[0].String()
+94 -328
View File
@@ -109,28 +109,19 @@ func (r *extensionRuntime) validatePath(path string) (string, error) {
func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "URL and output path are required",
})
return r.jsError("URL and output path are required")
}
urlStr := call.Arguments[0].String()
outputPath := call.Arguments[1].String()
if err := r.validateDomain(urlStr); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
fullPath, err := r.validatePath(outputPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
var onProgress goja.Callable
@@ -187,10 +178,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create directory: %v", err),
})
return r.jsError("failed to create directory: %v", err)
}
client := r.downloadClient
@@ -212,10 +200,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
req = r.bindDownloadCancelContext(req)
@@ -228,18 +213,12 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
resp, err := client.Do(req)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("HTTP error: %d", resp.StatusCode),
})
return r.jsError("HTTP error: %d", resp.StatusCode)
}
// Stream into a staged sibling and promote via rename on success so a
@@ -249,10 +228,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
os.Remove(stagedPath)
out, err := os.Create(stagedPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create file: %v", err),
})
return r.jsError("failed to create file: %v", err)
}
promoted := false
defer func() {
@@ -293,21 +269,12 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
written += int64(nw)
if ew != nil {
if ew == ErrDownloadCancelled {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "download cancelled",
})
return r.jsError("download cancelled")
}
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to write file: %v", ew),
})
return r.jsError("failed to write file: %v", ew)
}
if nr != nw {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "short write",
})
return r.jsError("short write")
}
if onProgress != nil && contentLength > 0 {
@@ -316,10 +283,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
}
if er != nil {
if er != io.EOF {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to read response: %v", er),
})
return r.jsError("failed to read response: %v", er)
}
break
}
@@ -336,32 +300,22 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
// Sync before the promote rename so a power loss right after the rename
// cannot leave a truncated file under the final name.
if err := out.Sync(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to sync file: %v", err),
})
return r.jsError("failed to sync file: %v", err)
}
if err := out.Close(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to finalize file: %v", err),
})
return r.jsError("failed to finalize file: %v", err)
}
if err := os.Rename(stagedPath, fullPath); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to publish file: %v", err),
})
return r.jsError("failed to publish file: %v", err)
}
promoted = true
syncDir(filepath.Dir(fullPath))
GoLog("[Extension:%s] Downloaded %d bytes to %s\n", r.extensionID, written, fullPath)
return r.vm.ToValue(map[string]any{
"success": true,
"path": fullPath,
"size": written,
return r.jsSuccess(map[string]any{
"path": fullPath,
"size": written,
})
}
@@ -375,10 +329,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
// First, get the total content length with a small probe request
probeReq, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("chunked: probe request error: %v", err),
})
return r.jsError("chunked: probe request error: %v", err)
}
probeReq = r.bindDownloadCancelContext(probeReq)
probeReq.Header.Set("User-Agent", ua)
@@ -391,19 +342,13 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
probeResp, err := client.Do(probeReq)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("chunked: probe error: %v", err),
})
return r.jsError("chunked: probe error: %v", err)
}
io.Copy(io.Discard, probeResp.Body)
probeResp.Body.Close()
if probeResp.StatusCode != 206 && probeResp.StatusCode != 200 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("chunked: probe HTTP %d", probeResp.StatusCode),
})
return r.jsError("chunked: probe HTTP %d", probeResp.StatusCode)
}
// Parse Content-Range to get total size: "bytes 0-1/TOTAL"
@@ -432,10 +377,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
os.Remove(stagedPath)
out, err := os.Create(stagedPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create file: %v", err),
})
return r.jsError("failed to create file: %v", err)
}
promoted := false
defer func() {
@@ -476,10 +418,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
for retry := 0; retry < maxRetries; retry++ {
chunkReq, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("chunked: request error at offset %d: %v", offset, err),
})
return r.jsError("chunked: request error at offset %d: %v", offset, err)
}
chunkReq = r.bindDownloadCancelContext(chunkReq)
chunkReq.Header.Set("User-Agent", ua)
@@ -496,10 +435,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
time.Sleep(time.Duration(retry+1) * time.Second)
continue
}
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("chunked: error at offset %d after %d retries: %v", offset, maxRetries, chunkErr),
})
return r.jsError("chunked: error at offset %d after %d retries: %v", offset, maxRetries, chunkErr)
}
if chunkResp.StatusCode == 206 || chunkResp.StatusCode == 200 {
@@ -516,10 +452,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
}
}
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("chunked: HTTP %d at offset %d", chunkResp.StatusCode, offset),
})
return r.jsError("chunked: HTTP %d at offset %d", chunkResp.StatusCode, offset)
}
chunkWritten := int64(0)
@@ -538,22 +471,13 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
if ew != nil {
chunkResp.Body.Close()
if ew == ErrDownloadCancelled {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "download cancelled",
})
return r.jsError("download cancelled")
}
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to write file: %v", ew),
})
return r.jsError("failed to write file: %v", ew)
}
if nr != nw {
chunkResp.Body.Close()
return r.vm.ToValue(map[string]any{
"success": false,
"error": "short write",
})
return r.jsError("short write")
}
if onProgress != nil && totalSize > 0 {
@@ -563,10 +487,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
if er != nil {
if er != io.EOF {
chunkResp.Body.Close()
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to read chunk at offset %d: %v", offset, er),
})
return r.jsError("failed to read chunk at offset %d: %v", offset, er)
}
break
}
@@ -602,32 +523,22 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
// Sync before the promote rename so a power loss right after the rename
// cannot leave a truncated file under the final name.
if err := out.Sync(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to sync file: %v", err),
})
return r.jsError("failed to sync file: %v", err)
}
if err := out.Close(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to finalize file: %v", err),
})
return r.jsError("failed to finalize file: %v", err)
}
if err := os.Rename(stagedPath, fullPath); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to publish file: %v", err),
})
return r.jsError("failed to publish file: %v", err)
}
promoted = true
syncDir(filepath.Dir(fullPath))
GoLog("[Extension:%s] Chunked download complete: %d bytes to %s\n", r.extensionID, totalWritten, fullPath)
return r.vm.ToValue(map[string]any{
"success": true,
"path": fullPath,
"size": totalWritten,
return r.jsSuccess(map[string]any{
"path": fullPath,
"size": totalWritten,
})
}
@@ -648,79 +559,52 @@ func (r *extensionRuntime) fileExists(call goja.FunctionCall) goja.Value {
func (r *extensionRuntime) fileDelete(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "path is required",
})
return r.jsError("path is required")
}
path := call.Arguments[0].String()
fullPath, err := r.validatePath(path)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
if err := os.Remove(fullPath); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
})
return r.jsSuccess(nil)
}
func (r *extensionRuntime) fileRead(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "path is required",
})
return r.jsError("path is required")
}
path := call.Arguments[0].String()
fullPath, err := r.validatePath(path)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
data, err := os.ReadFile(fullPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
"data": string(data),
return r.jsSuccess(map[string]any{
"data": string(data),
})
}
func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "path is required",
})
return r.jsError("path is required")
}
path := call.Arguments[0].String()
fullPath, err := r.validatePath(path)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
options := parseRuntimeOptionsArgument(call, 1)
@@ -728,26 +612,17 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
length := runtimeOptionInt64(options, "length", -1)
encoding := runtimeOptionString(options, "encoding", "base64")
if offset < 0 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "offset must be >= 0",
})
return r.jsError("offset must be >= 0")
}
file, err := os.Open(fullPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
size := info.Size()
@@ -755,10 +630,7 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
offset = size
}
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to seek file: %v", err),
})
return r.jsError("failed to seek file: %v", err)
}
var data []byte
@@ -769,19 +641,13 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
buf := make([]byte, int(length))
n, readErr := file.Read(buf)
if readErr != nil && readErr != io.EOF {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to read file: %v", readErr),
})
return r.jsError("failed to read file: %v", readErr)
}
data = buf[:n]
default:
data, err = io.ReadAll(file)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to read file: %v", err),
})
return r.jsError("failed to read file: %v", err)
}
}
@@ -789,8 +655,7 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
strings.EqualFold(strings.TrimSpace(encoding), "raw") {
// Return raw bytes as an ArrayBuffer to avoid base64 encode/decode of
// large payloads under the goja interpreter.
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"data": r.vm.NewArrayBuffer(data),
"bytes_read": len(data),
"offset": offset,
@@ -801,14 +666,10 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
encoded, err := encodeRuntimeBytes(data, encoding)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"data": encoded,
"bytes_read": len(data),
"offset": offset,
@@ -818,10 +679,7 @@ func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
}
func (r *extensionRuntime) fileWrite(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "path and data are required",
})
return r.jsError("path and data are required")
}
path := call.Arguments[0].String()
@@ -829,18 +687,12 @@ func (r *extensionRuntime) fileWrite(call goja.FunctionCall) goja.Value {
fullPath, err := r.validatePath(path)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create directory: %v", err),
})
return r.jsError("failed to create directory: %v", err)
}
// Full-content write: stage and rename so a kill mid-write cannot leave
@@ -848,40 +700,27 @@ func (r *extensionRuntime) fileWrite(call goja.FunctionCall) goja.Value {
stagedPath := stagedDownloadPath(fullPath)
if err := os.WriteFile(stagedPath, []byte(data), 0644); err != nil {
os.Remove(stagedPath)
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
if err := os.Rename(stagedPath, fullPath); err != nil {
os.Remove(stagedPath)
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
"path": fullPath,
return r.jsSuccess(map[string]any{
"path": fullPath,
})
}
func (r *extensionRuntime) fileWriteBytes(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "path and data are required",
})
return r.jsError("path and data are required")
}
path := call.Arguments[0].String()
fullPath, err := r.validatePath(path)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
options := parseRuntimeOptionsArgument(call, 2)
@@ -892,32 +731,20 @@ func (r *extensionRuntime) fileWriteBytes(call goja.FunctionCall) goja.Value {
encoding := runtimeOptionString(options, "encoding", "base64")
if appendMode && hasOffset {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "append and offset cannot be used together",
})
return r.jsError("append and offset cannot be used together")
}
if offset < 0 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "offset must be >= 0",
})
return r.jsError("offset must be >= 0")
}
data, err := decodeRuntimeBytesValue(call.Arguments[1].Export(), encoding)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create directory: %v", err),
})
return r.jsError("failed to create directory: %v", err)
}
flags := os.O_CREATE | os.O_WRONLY
@@ -930,28 +757,19 @@ func (r *extensionRuntime) fileWriteBytes(call goja.FunctionCall) goja.Value {
file, err := os.OpenFile(fullPath, flags, 0644)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
defer file.Close()
if hasOffset && !appendMode {
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to seek file: %v", err),
})
return r.jsError("failed to seek file: %v", err)
}
}
written, err := file.Write(data)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
info, statErr := file.Stat()
@@ -960,8 +778,7 @@ func (r *extensionRuntime) fileWriteBytes(call goja.FunctionCall) goja.Value {
size = info.Size()
}
return r.vm.ToValue(map[string]any{
"success": true,
return r.jsSuccess(map[string]any{
"path": fullPath,
"bytes_written": written,
"size": size,
@@ -970,10 +787,7 @@ func (r *extensionRuntime) fileWriteBytes(call goja.FunctionCall) goja.Value {
func (r *extensionRuntime) fileCopy(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "source and destination paths are required",
})
return r.jsError("source and destination paths are required")
}
srcPath := call.Arguments[0].String()
@@ -981,72 +795,47 @@ func (r *extensionRuntime) fileCopy(call goja.FunctionCall) goja.Value {
fullSrc, err := r.validatePath(srcPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
fullDst, err := r.validatePath(dstPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
srcFile, err := os.Open(fullSrc)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to read source: %v", err),
})
return r.jsError("failed to read source: %v", err)
}
defer srcFile.Close()
dir := filepath.Dir(fullDst)
if err := os.MkdirAll(dir, 0755); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create directory: %v", err),
})
return r.jsError("failed to create directory: %v", err)
}
dstFile, err := os.OpenFile(fullDst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to open destination: %v", err),
})
return r.jsError("failed to open destination: %v", err)
}
if _, err := io.Copy(dstFile, srcFile); err != nil {
_ = dstFile.Close()
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to copy file: %v", err),
})
return r.jsError("failed to copy file: %v", err)
}
if err := dstFile.Close(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to finalize destination: %v", err),
})
return r.jsError("failed to finalize destination: %v", err)
}
return r.vm.ToValue(map[string]any{
"success": true,
"path": fullDst,
return r.jsSuccess(map[string]any{
"path": fullDst,
})
}
func (r *extensionRuntime) fileMove(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "source and destination paths are required",
})
return r.jsError("source and destination paths are required")
}
srcPath := call.Arguments[0].String()
@@ -1054,68 +843,45 @@ func (r *extensionRuntime) fileMove(call goja.FunctionCall) goja.Value {
fullSrc, err := r.validatePath(srcPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
fullDst, err := r.validatePath(dstPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
dir := filepath.Dir(fullDst)
if err := os.MkdirAll(dir, 0755); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to create directory: %v", err),
})
return r.jsError("failed to create directory: %v", err)
}
if err := os.Rename(fullSrc, fullDst); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to move file: %v", err),
})
return r.jsError("failed to move file: %v", err)
}
return r.vm.ToValue(map[string]any{
"success": true,
"path": fullDst,
return r.jsSuccess(map[string]any{
"path": fullDst,
})
}
func (r *extensionRuntime) fileGetSize(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "path is required",
})
return r.jsError("path is required")
}
path := call.Arguments[0].String()
fullPath, err := r.validatePath(path)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
info, err := os.Stat(fullPath)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
"size": info.Size(),
return r.jsSuccess(map[string]any{
"size": info.Size(),
})
}
+10 -12
View File
@@ -35,6 +35,12 @@ func readExtensionHTTPResponseBody(resp *http.Response) ([]byte, error) {
return body, nil
}
func setDefaultExtensionUA(req *http.Request) {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Spotiflac-Extension/1.0")
}
}
func (r *extensionRuntime) validateDomain(urlStr string) error {
parsed, err := url.Parse(urlStr)
if err != nil {
@@ -106,9 +112,7 @@ func (r *extensionRuntime) httpGet(call goja.FunctionCall) goja.Value {
req.Header.Set(k, v)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Spotiflac-Extension/1.0")
}
setDefaultExtensionUA(req)
resp, err := r.httpClient.Do(req)
if err != nil {
@@ -201,9 +205,7 @@ func (r *extensionRuntime) httpPost(call goja.FunctionCall) goja.Value {
req.Header.Set(k, v)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Spotiflac-Extension/1.0")
}
setDefaultExtensionUA(req)
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
@@ -311,9 +313,7 @@ func (r *extensionRuntime) httpRequest(call goja.FunctionCall) goja.Value {
req.Header.Set(k, v)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Spotiflac-Extension/1.0")
}
setDefaultExtensionUA(req)
if bodyStr != "" && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
@@ -437,9 +437,7 @@ func (r *extensionRuntime) httpMethodShortcut(method string, call goja.FunctionC
for k, v := range headers {
req.Header.Set(k, v)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Spotiflac-Extension/1.0")
}
setDefaultExtensionUA(req)
if bodyStr != "" && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
+4 -15
View File
@@ -405,10 +405,7 @@ func (r *extensionRuntime) saveCredentials(creds map[string]any) error {
func (r *extensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "key and value are required",
})
return r.jsError("key and value are required")
}
key := call.Arguments[0].String()
@@ -416,10 +413,7 @@ func (r *extensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value {
if err := r.ensureCredentialsLoaded(); err != nil {
GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
r.credentialsMu.RLock()
@@ -429,15 +423,10 @@ func (r *extensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value {
if err := r.saveCredentials(nextCreds); err != nil {
GoLog("[Extension:%s] Credentials save error: %v\n", r.extensionID, err)
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
})
return r.jsSuccess(nil)
}
func (r *extensionRuntime) credentialsGet(call goja.FunctionCall) goja.Value {
+31 -34
View File
@@ -16,6 +16,24 @@ import (
"github.com/dop251/goja"
)
// jsError returns the standard {"success": false, "error": ...} extension
// response.
func (r *extensionRuntime) jsError(format string, args ...any) goja.Value {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf(format, args...),
})
}
// jsSuccess returns kv with "success": true added.
func (r *extensionRuntime) jsSuccess(kv map[string]any) goja.Value {
if kv == nil {
kv = map[string]any{}
}
kv["success"] = true
return r.vm.ToValue(kv)
}
func (r *extensionRuntime) base64Encode(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue("")
@@ -162,10 +180,7 @@ func (r *extensionRuntime) stringifyJSON(call goja.FunctionCall) goja.Value {
func (r *extensionRuntime) cryptoEncrypt(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "plaintext and key are required",
})
return r.jsError("plaintext and key are required")
}
plaintext := call.Arguments[0].String()
@@ -175,24 +190,17 @@ func (r *extensionRuntime) cryptoEncrypt(call goja.FunctionCall) goja.Value {
encrypted, err := encryptAES([]byte(plaintext), keyHash[:])
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
"data": base64.StdEncoding.EncodeToString(encrypted),
return r.jsSuccess(map[string]any{
"data": base64.StdEncoding.EncodeToString(encrypted),
})
}
func (r *extensionRuntime) cryptoDecrypt(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "ciphertext and key are required",
})
return r.jsError("ciphertext and key are required")
}
ciphertextB64 := call.Arguments[0].String()
@@ -200,25 +208,18 @@ func (r *extensionRuntime) cryptoDecrypt(call goja.FunctionCall) goja.Value {
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextB64)
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "invalid base64 ciphertext",
})
return r.jsError("invalid base64 ciphertext")
}
keyHash := sha256.Sum256([]byte(keyStr))
decrypted, err := decryptAES(ciphertext, keyHash[:])
if err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": "invalid base64 ciphertext",
})
return r.jsError("invalid base64 ciphertext")
}
return r.vm.ToValue(map[string]any{
"success": true,
"data": string(decrypted),
return r.jsSuccess(map[string]any{
"data": string(decrypted),
})
}
@@ -232,16 +233,12 @@ func (r *extensionRuntime) cryptoGenerateKey(call goja.FunctionCall) goja.Value
key := make([]byte, length)
if _, err := rand.Read(key); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": err.Error(),
})
return r.jsError("%s", err.Error())
}
return r.vm.ToValue(map[string]any{
"success": true,
"key": base64.StdEncoding.EncodeToString(key),
"hex": hex.EncodeToString(key),
return r.jsSuccess(map[string]any{
"key": base64.StdEncoding.EncodeToString(key),
"hex": hex.EncodeToString(key),
})
}
+13
View File
@@ -0,0 +1,13 @@
package gobackend
import "encoding/json"
// marshalJSONString marshals v and returns it as a string, the shape every
// gomobile export returns.
func marshalJSONString(v any) (string, error) {
jsonBytes, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
+1 -2
View File
@@ -846,8 +846,7 @@ func joinVorbisCommentValues(values []string) string {
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
return CheckFileExists(path)
}
func ExtractCoverArt(filePath string) ([]byte, error) {