refactor(bridge): remove dead cross-layer platform bridge methods

This commit is contained in:
zarzet
2026-07-26 18:32:40 +07:00
parent d38a368f63
commit 901fa34d80
16 changed files with 63 additions and 716 deletions
@@ -1826,66 +1826,6 @@ class MainActivity: FlutterFragmentActivity() {
return result.toString()
}
private fun runPostProcessingSaf(fileUriStr: String, metadataJson: String): String {
val uri = Uri.parse(fileUriStr)
val doc = DocumentFile.fromSingleUri(this, uri)
?: return errorJson("SAF file not found")
val tempInput = copyUriToTemp(uri) ?: return errorJson("Failed to copy SAF file to temp")
val tempDir = File(tempInput).parentFile?.absolutePath ?: ""
if (tempDir.isNotBlank()) {
try {
Gobackend.allowDownloadDir(tempDir)
} catch (_: Exception) {}
}
val response = Gobackend.runPostProcessingJSON(tempInput, metadataJson)
val respObj = JSONObject(response)
if (!respObj.optBoolean("success", false)) {
try {
File(tempInput).delete()
} catch (_: Exception) {}
return response
}
val newPath = respObj.optString("new_file_path", "")
val outputPath = if (newPath.isNotBlank()) newPath else tempInput
val outputFile = File(outputPath)
if (!outputFile.exists()) {
try {
File(tempInput).delete()
} catch (_: Exception) {}
respObj.put("success", false)
respObj.put("error", "postProcess output not found")
return respObj.toString()
}
val newName = outputFile.name
if (!newName.isNullOrBlank() && doc.name != null && doc.name != newName) {
try {
doc.renameTo(newName)
} catch (_: Exception) {}
}
val writeOk = writeUriFromPath(uri, outputFile.absolutePath)
if (!writeOk) {
respObj.put("success", false)
respObj.put("error", "failed to write postProcess output to SAF")
return respObj.toString()
}
try {
if (outputPath != tempInput) {
outputFile.delete()
}
File(tempInput).delete()
} catch (_: Exception) {}
respObj.put("new_file_path", uri.toString())
respObj.put("file_path", uri.toString())
return respObj.toString()
}
private fun runPostProcessingSafV2(fileUriStr: String, metadataJson: String): String {
val uri = Uri.parse(fileUriStr)
val doc = DocumentFile.fromSingleUri(this, uri)
@@ -2255,32 +2195,12 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
"getDownloadProgress" -> {
val response = withContext(Dispatchers.IO) {
Gobackend.getDownloadProgress()
}
result.success(parseJsonPayload(response))
}
"getAllDownloadProgress" -> {
val response = withContext(Dispatchers.IO) {
Gobackend.getAllDownloadProgress()
}
result.success(parseJsonPayload(response))
}
"initItemProgress" -> {
val itemId = call.argument<String>("item_id") ?: ""
withContext(Dispatchers.IO) {
Gobackend.initItemProgress(itemId)
}
result.success(null)
}
"finishItemProgress" -> {
val itemId = call.argument<String>("item_id") ?: ""
withContext(Dispatchers.IO) {
Gobackend.finishItemProgress(itemId)
}
result.success(null)
}
"clearItemProgress" -> {
val itemId = call.argument<String>("item_id") ?: ""
withContext(Dispatchers.IO) {
@@ -2324,14 +2244,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(null)
}
"checkDuplicate" -> {
val outputDir = call.argument<String>("output_dir") ?: ""
val isrc = call.argument<String>("isrc") ?: ""
val response = withContext(Dispatchers.IO) {
Gobackend.checkDuplicate(outputDir, isrc)
}
result.success(response)
}
"checkDuplicatesBatch" -> {
val outputDir = call.argument<String>("output_dir") ?: ""
val tracksJson = call.argument<String>("tracks") ?: "[]"
@@ -2588,16 +2500,6 @@ class MainActivity: FlutterFragmentActivity() {
result.error("share_failed", e.message, null)
}
}
"fetchLyrics" -> {
val spotifyId = call.argument<String>("spotify_id") ?: ""
val trackName = call.argument<String>("track_name") ?: ""
val artistName = call.argument<String>("artist_name") ?: ""
val durationMs = call.argument<Int>("duration_ms")?.toLong() ?: 0L
val response = withContext(Dispatchers.IO) {
Gobackend.fetchLyrics(spotifyId, trackName, artistName, durationMs)
}
result.success(response)
}
"getLyricsLRC" -> {
val spotifyId = call.argument<String>("spotify_id") ?: ""
val trackName = call.argument<String>("track_name") ?: ""
@@ -3116,14 +3018,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(null)
}
"getDeezerRelatedArtists" -> {
val artistId = call.argument<String>("artist_id") ?: ""
val limit = call.argument<Int>("limit") ?: 12
val response = withContext(Dispatchers.IO) {
Gobackend.getDeezerRelatedArtists(artistId, limit.toLong())
}
result.success(response)
}
"getProviderMetadata" -> {
val providerId = call.argument<String>("provider_id") ?: ""
val resourceType = call.argument<String>("resource_type") ?: ""
@@ -3354,14 +3248,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
"searchTracksWithExtensions" -> {
val query = call.argument<String>("query") ?: ""
val limit = call.argument<Int>("limit") ?: 20
val response = withContext(Dispatchers.IO) {
Gobackend.searchTracksWithExtensionsJSON(query, limit.toLong())
}
result.success(response)
}
"searchTracksWithMetadataProviders" -> {
val query = call.argument<String>("query") ?: ""
val limit = call.argument<Int>("limit") ?: 20
@@ -3499,12 +3385,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(null)
}
"getSearchProviders" -> {
val response = withContext(Dispatchers.IO) {
Gobackend.getSearchProvidersJSON()
}
result.success(response)
}
"handleURLWithExtension" -> {
val url = call.argument<String>("url") ?: ""
val response = withContext(Dispatchers.IO) {
@@ -3519,21 +3399,11 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
"getURLHandlers" -> {
"getTrackPlatformLinks" -> {
val spotifyId = call.argument<String>("spotify_id") ?: ""
val isrc = call.argument<String>("isrc") ?: ""
val response = withContext(Dispatchers.IO) {
Gobackend.getURLHandlersJSON()
}
result.success(response)
}
"runPostProcessing" -> {
val filePath = call.argument<String>("file_path") ?: ""
val metadataJson = call.argument<String>("metadata") ?: ""
val response = withContext(Dispatchers.IO) {
if (filePath.startsWith("content://")) {
runPostProcessingSaf(filePath, metadataJson)
} else {
Gobackend.runPostProcessingJSON(filePath, metadataJson)
}
Gobackend.getTrackPlatformLinksJSON(spotifyId, isrc)
}
result.success(response)
}
@@ -3562,12 +3432,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
"getPostProcessingProviders" -> {
val response = withContext(Dispatchers.IO) {
Gobackend.getPostProcessingProvidersJSON()
}
result.success(response)
}
"initExtensionRepo" -> {
val cacheDir = call.argument<String>("cache_dir") ?: ""
withContext(Dispatchers.IO) {
@@ -3637,13 +3501,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
"getExtensionBrowseCategories" -> {
val extensionId = call.argument<String>("extension_id") ?: ""
val response = withContext(Dispatchers.IO) {
Gobackend.getExtensionBrowseCategoriesJSON(extensionId)
}
result.success(response)
}
"setLibraryCoverCacheDir" -> {
val cacheDir = call.argument<String>("cache_dir") ?: ""
withContext(Dispatchers.IO) {
-61
View File
@@ -19,7 +19,6 @@ const (
deezerTrackURL = deezerBaseURL + "/track/%s"
deezerAlbumURL = deezerBaseURL + "/album/%s"
deezerArtistURL = deezerBaseURL + "/artist/%s"
deezerArtistRelatedURL = deezerBaseURL + "/artist/%s/related"
deezerPlaylistURL = deezerBaseURL + "/playlist/%s"
deezerCacheTTL = 10 * time.Minute
@@ -846,66 +845,6 @@ func (c *DeezerClient) fetchAlbumTrackCounts(ctx context.Context, albums []Artis
wg.Wait()
}
func (c *DeezerClient) GetRelatedArtists(ctx context.Context, artistID string, limit int) ([]SearchArtistResult, error) {
normalizedArtistID := strings.TrimSpace(strings.TrimPrefix(artistID, "deezer:"))
if normalizedArtistID == "" {
return nil, fmt.Errorf("invalid Deezer artist ID")
}
effectiveLimit := limit
if effectiveLimit <= 0 {
effectiveLimit = 12
}
relatedURL := fmt.Sprintf("%s?limit=%d", fmt.Sprintf(deezerArtistRelatedURL, normalizedArtistID), effectiveLimit)
var relatedResp struct {
Data []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Picture string `json:"picture"`
PictureMedium string `json:"picture_medium"`
PictureBig string `json:"picture_big"`
PictureXL string `json:"picture_xl"`
NbFan int `json:"nb_fan"`
} `json:"data"`
Error *struct {
Type string `json:"type"`
Message string `json:"message"`
Code int `json:"code"`
} `json:"error,omitempty"`
}
if err := c.getJSON(ctx, relatedURL, &relatedResp); err != nil {
return nil, err
}
if relatedResp.Error != nil {
return nil, fmt.Errorf("deezer related artists error: %s", relatedResp.Error.Message)
}
result := make([]SearchArtistResult, 0, len(relatedResp.Data))
for _, artist := range relatedResp.Data {
imageURL := artist.PictureXL
if imageURL == "" {
imageURL = artist.PictureBig
}
if imageURL == "" {
imageURL = artist.PictureMedium
}
if imageURL == "" {
imageURL = artist.Picture
}
result = append(result, SearchArtistResult{
ID: fmt.Sprintf("deezer:%d", artist.ID),
Name: artist.Name,
Images: imageURL,
Followers: artist.NbFan,
Popularity: 0,
})
}
return result, nil
}
func (c *DeezerClient) GetPlaylist(ctx context.Context, playlistID string) (*PlaylistResponsePayload, error) {
playlistURL := fmt.Sprintf(deezerPlaylistURL, playlistID)
-11
View File
@@ -78,17 +78,6 @@ func TestDeezerClientWithFakeHTTP(t *testing.T) {
t.Fatalf("cached artist = %#v/%v", cachedArtist, err)
}
related, err := client.GetRelatedArtists(ctx, "deezer:301", 3)
if err != nil {
t.Fatalf("GetRelatedArtists: %v", err)
}
if len(related) != 1 || related[0].ID != "deezer:302" {
t.Fatalf("related = %#v", related)
}
if _, err := client.GetRelatedArtists(ctx, "", 0); err == nil {
t.Fatal("expected invalid related artist ID")
}
playlist, err := client.GetPlaylist(ctx, "401")
if err != nil {
t.Fatalf("GetPlaylist: %v", err)
+10 -11
View File
@@ -63,6 +63,16 @@ func SetSongLinkNetworkOptions(allowHTTP, insecureTLS bool) {
SetNetworkCompatibilityOptions(allowHTTP, insecureTLS)
}
// GetTrackPlatformLinksJSON returns {"platforms": {platformID: url}} for a
// track, resolved via song.link (memory-cached; either ID may be empty).
func GetTrackPlatformLinksJSON(spotifyTrackID string, isrc string) (string, error) {
links, err := NewSongLinkClient().GetTrackPlatformLinks(spotifyTrackID, isrc)
if err != nil {
return "", err
}
return marshalJSONString(map[string]any{"platforms": links})
}
func SetDownloadDirectory(path string) error {
return setDownloadDir(path)
}
@@ -74,17 +84,6 @@ func AllowDownloadDir(path string) {
AddAllowedDownloadDir(path)
}
func CheckDuplicate(outputDir, isrc string) (string, error) {
existingFile, exists := CheckISRCExists(outputDir, isrc)
result := map[string]any{
"exists": exists,
"filepath": existingFile,
}
return marshalJSONString(result)
}
func CheckDuplicatesBatch(outputDir, tracksJSON string) (string, error) {
return CheckFilesExistParallel(outputDir, tracksJSON)
}
-16
View File
@@ -17,22 +17,6 @@ func GetTrackCacheSize() int {
func ClearTrackIDCache() {
}
func GetDeezerRelatedArtists(artistID string, limit int) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
client := GetDeezerClient()
artists, err := client.GetRelatedArtists(ctx, artistID, limit)
if err != nil {
return "", err
}
resp := map[string]any{
"artists": artists,
}
return marshalJSONString(resp)
}
func GetDeezerMetadata(resourceType, resourceID string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
-10
View File
@@ -383,12 +383,6 @@ func DownloadByStrategy(requestJSON string) (string, error) {
return errorResponse("Extension providers are disabled; built-in download providers have been retired")
}
func GetDownloadProgress() string {
progress := getProgress()
jsonBytes, _ := json.Marshal(progress)
return string(jsonBytes)
}
func GetAllDownloadProgress() string {
return GetMultiProgress()
}
@@ -401,10 +395,6 @@ func InitItemProgress(itemID string) {
StartItemProgress(itemID)
}
func FinishItemProgress(itemID string) {
CompleteItemProgress(itemID)
}
func ClearItemProgress(itemID string) {
RemoveItemProgress(itemID)
}
-92
View File
@@ -432,16 +432,6 @@ func SetExtensionSettingsJSON(extensionID, settingsJSON string) error {
return manager.InitializeExtension(extensionID, settings)
}
func SearchTracksWithExtensionsJSON(query string, limit int) (string, error) {
manager := getExtensionManager()
tracks, err := manager.SearchTracksWithExtensions(query, limit)
if err != nil {
return "", err
}
return marshalJSONString(tracks)
}
func SearchTracksWithMetadataProvidersJSON(query string, limit int, includeExtensions bool) (string, error) {
manager := getExtensionManager()
tracks, err := manager.SearchTracksWithMetadataProviders(query, limit, includeExtensions)
@@ -821,24 +811,6 @@ func CustomSearchWithExtensionJSONWithRequestID(extensionID, query string, optio
return marshalJSONString(result)
}
func GetSearchProvidersJSON() (string, error) {
manager := getExtensionManager()
providers := manager.GetSearchProviders()
result := make([]map[string]any, 0, len(providers))
for _, p := range providers {
result = append(result, map[string]any{
"id": p.extension.ID,
"display_name": p.extension.Manifest.DisplayName,
"placeholder": p.extension.Manifest.SearchBehavior.Placeholder,
"primary": p.extension.Manifest.SearchBehavior.Primary,
"icon": p.extension.Manifest.SearchBehavior.Icon,
})
}
return marshalJSONString(result)
}
func HandleURLWithExtensionJSON(url string) (string, error) {
manager := getExtensionManager()
resultWithID, err := manager.HandleURLWithExtension(url)
@@ -1025,39 +997,6 @@ func FindURLHandlerJSON(url string) string {
return handler.extension.ID
}
func GetURLHandlersJSON() (string, error) {
manager := getExtensionManager()
handlers := manager.GetURLHandlers()
result := make([]map[string]any, 0, len(handlers))
for _, h := range handlers {
result = append(result, map[string]any{
"id": h.extension.ID,
"display_name": h.extension.Manifest.DisplayName,
"patterns": h.extension.Manifest.URLHandler.Patterns,
})
}
return marshalJSONString(result)
}
func RunPostProcessingJSON(filePath, metadataJSON string) (string, error) {
var metadata map[string]any
if metadataJSON != "" {
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
metadata = make(map[string]any)
}
}
manager := getExtensionManager()
result, err := manager.RunPostProcessing(filePath, metadata)
if err != nil {
return "", err
}
return marshalJSONString(result)
}
func RunPostProcessingV2JSON(inputJSON, metadataJSON string) (string, error) {
var metadata map[string]any
if metadataJSON != "" {
@@ -1082,33 +1021,6 @@ func RunPostProcessingV2JSON(inputJSON, metadataJSON string) (string, error) {
return marshalJSONString(result)
}
func GetPostProcessingProvidersJSON() (string, error) {
manager := getExtensionManager()
providers := manager.GetPostProcessingProviders()
result := make([]map[string]any, 0, len(providers))
for _, p := range providers {
hooks := make([]map[string]any, 0)
for _, h := range p.extension.Manifest.GetPostProcessingHooks() {
hooks = append(hooks, map[string]any{
"id": h.ID,
"name": h.Name,
"description": h.Description,
"default_enabled": h.DefaultEnabled,
"supported_formats": h.SupportedFormats,
})
}
result = append(result, map[string]any{
"id": p.extension.ID,
"display_name": p.extension.Manifest.DisplayName,
"hooks": hooks,
})
}
return marshalJSONString(result)
}
func callExtensionFunctionJSON(extensionID, functionName string, timeout time.Duration) (string, error) {
return callExtensionFunctionJSONWithRequestID(extensionID, functionName, timeout, "")
}
@@ -1187,10 +1099,6 @@ func GetExtensionHomeFeedJSONWithRequestID(extensionID, requestID string) (strin
return callExtensionFunctionJSONWithRequestID(extensionID, "getHomeFeed", 60*time.Second, requestID)
}
func GetExtensionBrowseCategoriesJSON(extensionID string) (string, error) {
return callExtensionFunctionJSON(extensionID, "getBrowseCategories", 30*time.Second)
}
func CancelExtensionRequestJSON(requestID string) {
cancelExtensionRequest(requestID)
}
-19
View File
@@ -7,25 +7,6 @@ import (
"strings"
)
func FetchLyrics(spotifyID, trackName, artistName string, durationMs int64) (string, error) {
client := NewLyricsClient()
durationSec := float64(durationMs) / 1000.0
lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec)
if err != nil {
return "", err
}
result := map[string]any{
"success": true,
"source": lyrics.Source,
"sync_type": lyrics.SyncType,
"lines": lyrics.Lines,
"instrumental": lyrics.Instrumental,
}
return marshalJSONString(result)
}
func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string, durationMs int64) (string, error) {
if filePath != "" {
lyrics, err := ExtractLyrics(filePath)
@@ -20,9 +20,6 @@ func TestLyricsExportWrappersWithoutNetwork(t *testing.T) {
t.Fatal(err)
}
if jsonText, err := FetchLyrics("spotify-1", "Song Instrumental", "Artist", 180000); err != nil || !strings.Contains(jsonText, `"instrumental":true`) {
t.Fatalf("FetchLyrics instrumental = %q/%v", jsonText, err)
}
if lrc, err := GetLyricsLRC("spotify-1", "Song Instrumental", "Artist", "", 180000); err != nil || lrc != "[instrumental:true]" {
t.Fatalf("GetLyricsLRC instrumental = %q/%v", lrc, err)
}
@@ -112,6 +109,19 @@ func TestSongLinkExportWrappersWithFakeClient(t *testing.T) {
t.Fatal("songLinkExtractDeezerTrackID mismatch")
}
if linksJSON, err := GetTrackPlatformLinksJSON("spotify-1", ""); err != nil ||
!strings.Contains(linksJSON, `"tidal":"https://listen.tidal.com/track/202"`) ||
!strings.Contains(linksJSON, `"spotify":`) {
t.Fatalf("GetTrackPlatformLinksJSON = %q/%v", linksJSON, err)
}
// Second call must come from the links cache, not a new request.
if cached, hit, cachedErr := trackPlatformLinksCacheLookup(GetSongLinkRegion() + "|spotify:spotify-1"); !hit || cachedErr || cached["tidal"] == "" {
t.Fatalf("trackPlatformLinksCacheLookup = %#v hit=%v err=%v", cached, hit, cachedErr)
}
if _, err := GetTrackPlatformLinksJSON("", ""); err == nil {
t.Fatal("GetTrackPlatformLinksJSON with empty IDs should error")
}
deezerClient = &DeezerClient{
httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
body := fakeDeezerResponse(req.URL.Path, req.URL.RawQuery)
+1 -26
View File
@@ -134,10 +134,9 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) {
}
InitItemProgress("item-1")
FinishItemProgress("item-1")
ClearItemProgress("item-1")
CancelDownload("item-1")
if GetDownloadProgress() == "" || GetAllDownloadProgress() == "" || GetAllDownloadProgressDelta(0) == "" {
if GetAllDownloadProgress() == "" || GetAllDownloadProgressDelta(0) == "" {
t.Fatal("expected progress JSON")
}
CleanupConnections()
@@ -194,9 +193,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) {
if err := SetDownloadDirectory(dir); err != nil {
t.Fatalf("SetDownloadDirectory: %v", err)
}
if duplicateJSON, err := CheckDuplicate(dir, ""); err != nil || !strings.Contains(duplicateJSON, "exists") {
t.Fatalf("CheckDuplicate = %q/%v", duplicateJSON, err)
}
if batchJSON, err := CheckDuplicatesBatch(dir, `[{"isrc":"","track_name":"Song","artist_name":"Artist"}]`); err != nil || !strings.Contains(batchJSON, "Song") {
t.Fatalf("CheckDuplicatesBatch = %q/%v", batchJSON, err)
}
@@ -262,9 +258,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) {
t.Fatal("expected settings JSON error")
}
if jsonText, err := SearchTracksWithExtensionsJSON("song", 5); err != nil || !strings.Contains(jsonText, "search-1") {
t.Fatalf("SearchTracksWithExtensionsJSON = %q/%v", jsonText, err)
}
if jsonText, err := SearchTracksWithMetadataProvidersJSON("song", 5, true); err != nil || !strings.Contains(jsonText, "search-1") {
t.Fatalf("SearchTracksWithMetadataProvidersJSON = %q/%v", jsonText, err)
}
@@ -362,9 +355,6 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) {
if _, err := GetDeezerMetadata("bad", "1"); err == nil {
t.Fatal("expected unsupported Deezer metadata type")
}
if jsonText, err := GetDeezerRelatedArtists("301", 2); err != nil || !strings.Contains(jsonText, "Related") {
t.Fatalf("GetDeezerRelatedArtists = %q/%v", jsonText, err)
}
if jsonText, err := GetDeezerExtendedMetadata("101"); err != nil || !strings.Contains(jsonText, "Label") {
t.Fatalf("GetDeezerExtendedMetadata = %q/%v", jsonText, err)
}
@@ -385,37 +375,22 @@ func TestExportsJSONWrappersAndExtensionManagerSurface(t *testing.T) {
if customJSON, err := CustomSearchWithExtensionJSONWithRequestID(ext.ID, "needle", `not-json`, "req-custom"); err != nil || !strings.Contains(customJSON, "custom-1") {
t.Fatalf("CustomSearchWithExtensionJSONWithRequestID = %q/%v", customJSON, err)
}
if providersJSON, err := GetSearchProvidersJSON(); err != nil || !strings.Contains(providersJSON, "coverage-ext") {
t.Fatalf("GetSearchProvidersJSON = %q/%v", providersJSON, err)
}
if found := FindURLHandlerJSON("https://example.test/track/1"); found != ext.ID {
t.Fatalf("FindURLHandlerJSON = %q", found)
}
if handlersJSON, err := GetURLHandlersJSON(); err != nil || !strings.Contains(handlersJSON, "coverage-ext") {
t.Fatalf("GetURLHandlersJSON = %q/%v", handlersJSON, err)
}
if handledJSON, err := HandleURLWithExtensionJSON("https://example.test/track/1"); err != nil || !strings.Contains(handledJSON, "url-track") {
t.Fatalf("HandleURLWithExtensionJSON = %q/%v", handledJSON, err)
}
if postJSON, err := RunPostProcessingJSON(filepath.Join(dir, "song.flac"), `{"title":"Song"}`); err != nil || !strings.Contains(postJSON, "success") {
t.Fatalf("RunPostProcessingJSON = %q/%v", postJSON, err)
}
v2Input := `{"path":"` + escapeJSONPath(filepath.Join(dir, "song.flac")) + `","uri":"content://song","name":"song.flac","mime_type":"audio/flac","size":10}`
if postJSON, err := RunPostProcessingV2JSON(v2Input, `not-json`); err != nil || !strings.Contains(postJSON, "success") {
t.Fatalf("RunPostProcessingV2JSON = %q/%v", postJSON, err)
}
if postProviders, err := GetPostProcessingProvidersJSON(); err != nil || !strings.Contains(postProviders, "hook") {
t.Fatalf("GetPostProcessingProvidersJSON = %q/%v", postProviders, err)
}
if feedJSON, err := GetExtensionHomeFeedJSON(ext.ID); err != nil || !strings.Contains(feedJSON, "home-1") {
t.Fatalf("GetExtensionHomeFeedJSON = %q/%v", feedJSON, err)
}
if feedJSON, err := GetExtensionHomeFeedJSONWithRequestID(ext.ID, "req-home"); err != nil || !strings.Contains(feedJSON, "home-1") {
t.Fatalf("GetExtensionHomeFeedJSONWithRequestID = %q/%v", feedJSON, err)
}
if categoriesJSON, err := GetExtensionBrowseCategoriesJSON(ext.ID); err != nil || !strings.Contains(categoriesJSON, "cat-1") {
t.Fatalf("GetExtensionBrowseCategoriesJSON = %q/%v", categoriesJSON, err)
}
CancelExtensionRequestJSON("req-home")
storeDir := filepath.Join(dir, "store")
@@ -92,9 +92,9 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
t.Fatalf("url result = %#v", urlResult)
}
post, err := provider.PostProcess(filepath.Join(t.TempDir(), "song.flac"), map[string]any{"title": "Song"}, "hook")
post, err := provider.PostProcessV2(PostProcessInput{Path: filepath.Join(t.TempDir(), "song.flac")}, map[string]any{"title": "Song"}, "hook")
if err != nil {
t.Fatalf("PostProcess: %v", err)
t.Fatalf("PostProcessV2: %v", err)
}
if !post.Success || post.BitDepth != 24 || post.SampleRate != 96000 {
t.Fatalf("post = %#v", post)
+9 -25
View File
@@ -847,11 +847,9 @@ func postProcessInputMap(input PostProcessInput) map[string]any {
const PostProcessTimeout = 2 * time.Minute
// postProcessCommon backs both PostProcess (V1) and PostProcessV2. V1 probes
// only extension.postProcess (its original contract: V2-only extensions are
// not invoked via V1); V2 probes postProcessV2 first, then falls back to
// postProcess.
func (p *extensionProviderWrapper) postProcessCommon(input PostProcessInput, metadata map[string]any, hookID string, preferV2 bool) (*PostProcessResult, error) {
// PostProcessV2 probes extension.postProcessV2 first, then falls back to the
// legacy extension.postProcess contract.
func (p *extensionProviderWrapper) PostProcessV2(input PostProcessInput, metadata map[string]any, hookID string) (*PostProcessResult, error) {
if !p.extension.Manifest.HasPostProcessing() {
return nil, fmt.Errorf("extension '%s' does not support post-processing", p.extension.ID)
}
@@ -861,19 +859,13 @@ func (p *extensionProviderWrapper) postProcessCommon(input PostProcessInput, met
filePath := input.Path
perfName := "postProcess"
var invoke func(*goja.Runtime) (goja.Value, error)
if preferV2 {
perfName = "postProcessV2"
inputMap := postProcessInputMap(input)
invoke = func(vm *goja.Runtime) (goja.Value, error) {
if hasExtensionMethod(vm, "postProcessV2") {
return invokeExtensionMethod(vm, "postProcessV2", inputMap, metadata, hookID)
}
return invokeExtensionMethod(vm, "postProcess", filePath, metadata, hookID)
perfName := "postProcessV2"
inputMap := postProcessInputMap(input)
invoke := func(vm *goja.Runtime) (goja.Value, error) {
if hasExtensionMethod(vm, "postProcessV2") {
return invokeExtensionMethod(vm, "postProcessV2", inputMap, metadata, hookID)
}
} else {
invoke = extensionMethodInvocation("postProcess", filePath, metadata, hookID)
return invokeExtensionMethod(vm, "postProcess", filePath, metadata, hookID)
}
result, err := callExtension(p, extCallOpts{
@@ -898,14 +890,6 @@ func (p *extensionProviderWrapper) postProcessCommon(input PostProcessInput, met
return result, nil
}
func (p *extensionProviderWrapper) PostProcess(filePath string, metadata map[string]any, hookID string) (*PostProcessResult, error) {
return p.postProcessCommon(PostProcessInput{Path: filePath}, metadata, hookID, false)
}
func (p *extensionProviderWrapper) PostProcessV2(input PostProcessInput, metadata map[string]any, hookID string) (*PostProcessResult, error) {
return p.postProcessCommon(input, metadata, hookID, true)
}
type ExtLyricsResult struct {
Lines []ExtLyricsLine `json:"lines"`
SyncType string `json:"syncType"`
+3 -98
View File
@@ -34,49 +34,6 @@ func (m *extensionManager) GetDownloadProviders() []*extensionProviderWrapper {
return providers
}
func (m *extensionManager) SearchTracksWithExtensions(query string, limit int) ([]ExtTrackMetadata, error) {
providers := m.GetMetadataProviders()
if len(providers) == 0 {
return nil, nil
}
providerByID := make(map[string]*extensionProviderWrapper, len(providers))
orderedProviders := make([]*extensionProviderWrapper, 0, len(providers))
for _, provider := range providers {
providerByID[provider.extension.ID] = provider
}
for _, providerID := range GetMetadataProviderPriority() {
if provider := providerByID[providerID]; provider != nil {
orderedProviders = append(orderedProviders, provider)
delete(providerByID, providerID)
}
}
if len(providerByID) > 0 {
remainingIDs := make([]string, 0, len(providerByID))
for providerID := range providerByID {
remainingIDs = append(remainingIDs, providerID)
}
sort.Strings(remainingIDs)
for _, providerID := range remainingIDs {
orderedProviders = append(orderedProviders, providerByID[providerID])
}
}
var allTracks []ExtTrackMetadata
for _, provider := range orderedProviders {
result, err := provider.SearchTracks(query, limit)
if err != nil {
GoLog("[Extension] Search error from %s: %v\n", provider.extension.ID, err)
continue
}
if result != nil {
allTracks = append(allTracks, result.Tracks...)
}
}
return allTracks, nil
}
func metadataTrackDedupKey(track ExtTrackMetadata) string {
if isrc := strings.TrimSpace(track.ISRC); isrc != "" {
return "isrc:" + strings.ToUpper(isrc)
@@ -176,32 +133,6 @@ func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query stri
return tracks, nil
}
func (m *extensionManager) GetSearchProviders() []*extensionProviderWrapper {
m.mu.RLock()
defer m.mu.RUnlock()
var providers []*extensionProviderWrapper
for _, ext := range m.extensions {
if ext.Enabled && ext.Manifest.HasCustomSearch() && ext.Error == "" {
providers = append(providers, newExtensionProviderWrapper(ext))
}
}
return providers
}
func (m *extensionManager) GetURLHandlers() []*extensionProviderWrapper {
m.mu.RLock()
defer m.mu.RUnlock()
var providers []*extensionProviderWrapper
for _, ext := range m.extensions {
if ext.Enabled && ext.Manifest.HasURLHandler() && ext.Error == "" {
providers = append(providers, newExtensionProviderWrapper(ext))
}
}
return providers
}
func (m *extensionManager) FindURLHandler(url string) *extensionProviderWrapper {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -252,21 +183,13 @@ func (m *extensionManager) GetPostProcessingProviders() []*extensionProviderWrap
return providers
}
// runPostProcessingCommon backs both RunPostProcessing (V1) and
// RunPostProcessingV2. V1 delegates into this shared loop with an equivalent
// PostProcessInput; preferV2 controls whether each hook is invoked via
// provider.PostProcessV2 or provider.PostProcess, so V1 keeps calling
// PostProcess (not PostProcessV2) exactly as it did before.
func (m *extensionManager) runPostProcessingCommon(input PostProcessInput, metadata map[string]any, preferV2 bool) (*PostProcessResult, error) {
func (m *extensionManager) RunPostProcessingV2(input PostProcessInput, metadata map[string]any) (*PostProcessResult, error) {
providers := m.GetPostProcessingProviders()
if len(providers) == 0 {
return &PostProcessResult{Success: true, NewFilePath: input.Path, NewFileURI: input.URI}, nil
}
logTag := "[PostProcess]"
if preferV2 {
logTag = "[PostProcessV2]"
}
logTag := "[PostProcessV2]"
currentInput := input
for _, provider := range providers {
@@ -295,13 +218,7 @@ func (m *extensionManager) runPostProcessingCommon(input PostProcessInput, metad
GoLog("%s Running hook %s from %s on %s\n", logTag, hook.ID, provider.extension.ID, currentInput.Path)
var result *PostProcessResult
var err error
if preferV2 {
result, err = provider.PostProcessV2(currentInput, metadata, hook.ID)
} else {
result, err = provider.PostProcess(currentInput.Path, metadata, hook.ID)
}
result, err := provider.PostProcessV2(currentInput, metadata, hook.ID)
if err != nil {
GoLog("%s Hook %s failed: %v\n", logTag, hook.ID, err)
continue
@@ -360,18 +277,6 @@ func validatePostProcessResult(ext *loadedExtension, input PostProcessInput, res
return fmt.Errorf("replacement file path is outside allowed directories")
}
func (m *extensionManager) RunPostProcessing(filePath string, metadata map[string]any) (*PostProcessResult, error) {
result, err := m.runPostProcessingCommon(PostProcessInput{Path: filePath}, metadata, false)
if err != nil {
return result, err
}
return &PostProcessResult{Success: result.Success, NewFilePath: result.NewFilePath}, nil
}
func (m *extensionManager) RunPostProcessingV2(input PostProcessInput, metadata map[string]any) (*PostProcessResult, error) {
return m.runPostProcessingCommon(input, metadata, true)
}
func (m *extensionManager) GetLyricsProviders() []*extensionProviderWrapper {
m.mu.RLock()
defer m.mu.RUnlock()
-19
View File
@@ -108,25 +108,6 @@ func markMultiProgressDirtyIfChangedLocked(item *ItemProgress, before progressBr
}
}
func getProgress() DownloadProgress {
multiMu.RLock()
defer multiMu.RUnlock()
for _, item := range multiProgress.Items {
return DownloadProgress{
CurrentFile: item.ItemID,
Progress: item.Progress * 100,
BytesTotal: item.BytesTotal,
BytesReceived: item.BytesReceived,
IsDownloading: item.IsDownloading,
Status: item.Status,
Stage: item.Stage,
}
}
return DownloadProgress{}
}
func GetMultiProgress() string {
multiMu.RLock()
if !multiProgressDirty {
+5 -77
View File
@@ -440,26 +440,10 @@ import Gobackend
if let error = error { throw error }
return response
case "getDownloadProgress":
let response = GobackendGetDownloadProgress()
return parseJsonPayload(response as String? ?? "{}")
case "getAllDownloadProgress":
let response = GobackendGetAllDownloadProgress()
return parseJsonPayload(response as String? ?? "{}")
case "initItemProgress":
let args = call.arguments as! [String: Any]
let itemId = args["item_id"] as! String
GobackendInitItemProgress(itemId)
return nil
case "finishItemProgress":
let args = call.arguments as! [String: Any]
let itemId = args["item_id"] as! String
GobackendFinishItemProgress(itemId)
return nil
case "clearItemProgress":
let args = call.arguments as! [String: Any]
let itemId = args["item_id"] as! String
@@ -498,14 +482,6 @@ import Gobackend
GobackendSetAllowPrivateNetwork(allowed)
return nil
case "checkDuplicate":
let args = call.arguments as! [String: Any]
let outputDir = args["output_dir"] as! String
let isrc = args["isrc"] as! String
let response = GobackendCheckDuplicate(outputDir, isrc, &error)
if let error = error { throw error }
return response
case "checkDuplicatesBatch":
let args = call.arguments as! [String: Any]
let outputDir = args["output_dir"] as! String
@@ -541,16 +517,6 @@ import Gobackend
let response = GobackendSanitizeFilename(filename)
return response
case "fetchLyrics":
let args = call.arguments as! [String: Any]
let spotifyId = args["spotify_id"] as! String
let trackName = args["track_name"] as! String
let artistName = args["artist_name"] as! String
let durationMs = args["duration_ms"] as? Int64 ?? 0
let response = GobackendFetchLyrics(spotifyId, trackName, artistName, durationMs, &error)
if let error = error { throw error }
return response
case "getLyricsLRC":
let args = call.arguments as! [String: Any]
let spotifyId = args["spotify_id"] as! String
@@ -645,14 +611,6 @@ import Gobackend
if let error = error { throw error }
return response
case "getDeezerRelatedArtists":
let args = call.arguments as! [String: Any]
let artistId = args["artist_id"] as! String
let limit = args["limit"] as? Int ?? 12
let response = GobackendGetDeezerRelatedArtists(artistId, Int(limit), &error)
if let error = error { throw error }
return response
case "getProviderMetadata":
let args = call.arguments as! [String: Any]
let providerId = args["provider_id"] as! String
@@ -845,14 +803,6 @@ import Gobackend
if let error = error { throw error }
return response
case "searchTracksWithExtensions":
let args = call.arguments as! [String: Any]
let query = args["query"] as! String
let limit = args["limit"] as? Int ?? 20
let response = GobackendSearchTracksWithExtensionsJSON(query, Int(limit), &error)
if let error = error { throw error }
return response
case "searchTracksWithMetadataProviders":
let args = call.arguments as! [String: Any]
let query = args["query"] as! String
@@ -997,11 +947,6 @@ import Gobackend
GobackendCancelExtensionRequestJSON(requestId)
return nil
case "getSearchProviders":
let response = GobackendGetSearchProvidersJSON(&error)
if let error = error { throw error }
return response
case "handleURLWithExtension":
let args = call.arguments as! [String: Any]
let url = args["url"] as! String
@@ -1015,19 +960,14 @@ import Gobackend
let response = GobackendFindURLHandlerJSON(url)
return response
case "getURLHandlers":
let response = GobackendGetURLHandlersJSON(&error)
case "getTrackPlatformLinks":
let args = call.arguments as! [String: Any]
let spotifyId = args["spotify_id"] as? String ?? ""
let isrc = args["isrc"] as? String ?? ""
let response = GobackendGetTrackPlatformLinksJSON(spotifyId, isrc, &error)
if let error = error { throw error }
return response
case "runPostProcessing":
let args = call.arguments as! [String: Any]
let filePath = args["file_path"] as! String
let metadataJson = args["metadata"] as? String ?? ""
let response = GobackendRunPostProcessingJSON(filePath, metadataJson, &error)
if let error = error { throw error }
return response
case "runPostProcessingV2":
let args = call.arguments as! [String: Any]
let inputJson = args["input"] as? String ?? ""
@@ -1036,11 +976,6 @@ import Gobackend
if let error = error { throw error }
return response
case "getPostProcessingProviders":
let response = GobackendGetPostProcessingProvidersJSON(&error)
if let error = error { throw error }
return response
case "initExtensionRepo":
let args = call.arguments as! [String: Any]
let cacheDir = args["cache_dir"] as! String
@@ -1106,13 +1041,6 @@ import Gobackend
if let error = error { throw error }
return response
case "getExtensionBrowseCategories":
let args = call.arguments as! [String: Any]
let extensionId = args["extension_id"] as! String
let response = GobackendGetExtensionBrowseCategoriesJSON(extensionId, &error)
if let error = error { throw error }
return response
case "setLibraryCoverCacheDir":
let args = call.arguments as! [String: Any]
let cacheDir = args["cache_dir"] as! String
+16 -99
View File
@@ -501,11 +501,6 @@ class PlatformBridge {
return response;
}
static Future<Map<String, dynamic>> getDownloadProgress() async {
final result = await _channel.invokeMethod('getDownloadProgress');
return _decodeMapResult(result);
}
static Future<Map<String, dynamic>> getAllDownloadProgress() async {
final result = await _channel.invokeMethod('getAllDownloadProgress');
return _decodeMapResult(result);
@@ -521,14 +516,6 @@ class PlatformBridge {
await _channel.invokeMethod('exitApp');
}
static Future<void> initItemProgress(String itemId) async {
await _channel.invokeMethod('initItemProgress', {'item_id': itemId});
}
static Future<void> finishItemProgress(String itemId) async {
await _channel.invokeMethod('finishItemProgress', {'item_id': itemId});
}
static Future<void> clearItemProgress(String itemId) async {
await _channel.invokeMethod('clearItemProgress', {'item_id': itemId});
}
@@ -578,13 +565,6 @@ class PlatformBridge {
await _channel.invokeMethod('setAllowPrivateNetwork', {'allowed': allowed});
}
static Future<Map<String, dynamic>> checkDuplicate(
String outputDir,
String isrc,
) {
return _invokeMap('checkDuplicate', {'output_dir': outputDir, 'isrc': isrc});
}
static Future<String> buildFilename(
String template,
Map<String, dynamic> metadata,
@@ -734,20 +714,6 @@ class PlatformBridge {
return result as bool? ?? false;
}
static Future<Map<String, dynamic>> fetchLyrics(
String spotifyId,
String trackName,
String artistName, {
int durationMs = 0,
}) {
return _invokeMap('fetchLyrics', {
'spotify_id': spotifyId,
'track_name': trackName,
'artist_name': artistName,
'duration_ms': durationMs,
});
}
static Future<String> getLyricsLRC(
String spotifyId,
String trackName,
@@ -1111,16 +1077,6 @@ class PlatformBridge {
await _channel.invokeMethod('clearTrackCache');
}
static Future<Map<String, dynamic>> getDeezerRelatedArtists(
String artistId, {
int limit = 12,
}) {
return _invokeMap('getDeezerRelatedArtists', {
'artist_id': artistId,
'limit': limit,
});
}
static Future<Map<String, dynamic>> getProviderMetadata(
String providerId,
String resourceType,
@@ -1198,11 +1154,6 @@ class PlatformBridge {
);
}
static Future<List<Map<String, dynamic>>> getGoLogs() async {
final result = await _channel.invokeMethod('getLogs');
return _decodeMapListResult(result, 'getGoLogs');
}
static Future<Map<String, dynamic>> getGoLogsSince(int index) async {
final result = await _channel.invokeMethod('getLogsSince', {
'index': index,
@@ -1395,18 +1346,6 @@ class PlatformBridge {
return _decodeRequiredMapResult(result, 'invokeExtensionAction');
}
static Future<List<Map<String, dynamic>>> searchTracksWithExtensions(
String query, {
int limit = 20,
}) async {
_log.d('searchTracksWithExtensions: "$query"');
final result = await _channel.invokeMethod('searchTracksWithExtensions', {
'query': query,
'limit': limit,
});
return _decodeMapListResult(result, 'searchTracksWithExtensions');
}
static Future<List<Map<String, dynamic>>> searchTracksWithMetadataProviders(
String query, {
int limit = 20,
@@ -1602,11 +1541,6 @@ class PlatformBridge {
}
}
static Future<List<Map<String, dynamic>>> getSearchProviders() async {
final result = await _channel.invokeMethod('getSearchProviders');
return _decodeMapListResult(result, 'getSearchProviders');
}
static Future<Map<String, dynamic>?> handleURLWithExtension(
String url,
) async {
@@ -1727,9 +1661,22 @@ class PlatformBridge {
return result as String;
}
static Future<List<Map<String, dynamic>>> getURLHandlers() async {
final result = await _channel.invokeMethod('getURLHandlers');
return _decodeMapListResult(result, 'getURLHandlers');
/// Streaming-platform links for a track, keyed by song.link platform ID.
/// Served from the Go-side memory cache; throws when the lookup fails.
static Future<Map<String, String>> getTrackPlatformLinks({
String spotifyId = '',
String isrc = '',
}) async {
final result = await _channel.invokeMethod('getTrackPlatformLinks', {
'spotify_id': spotifyId,
'isrc': isrc,
});
final decoded = _decodeMapResult(result);
final platforms = decoded['platforms'];
if (platforms is! Map) return const {};
return platforms.map(
(key, value) => MapEntry(key.toString(), value.toString()),
);
}
static Future<Map<String, dynamic>?> getExtensionHomeFeed(
@@ -1774,21 +1721,6 @@ class PlatformBridge {
}
}
static Future<Map<String, dynamic>?> getExtensionBrowseCategories(
String extensionId,
) async {
try {
final result = await _channel.invokeMethod(
'getExtensionBrowseCategories',
{'extension_id': extensionId},
);
return _decodeNullableMapResult(result, 'getExtensionBrowseCategories');
} catch (e) {
_log.e('getExtensionBrowseCategories failed: $e');
return null;
}
}
static Future<void> setLibraryCoverCacheDir(String cacheDir) async {
_log.i('setLibraryCoverCacheDir: $cacheDir');
await _channel.invokeMethod('setLibraryCoverCacheDir', {
@@ -2107,16 +2039,6 @@ class PlatformBridge {
}
}
static Future<Map<String, dynamic>> runPostProcessing(
String filePath, {
Map<String, dynamic>? metadata,
}) {
return _invokeMap('runPostProcessing', {
'file_path': filePath,
'metadata': metadata != null ? jsonEncode(metadata) : '',
});
}
static Future<Map<String, dynamic>> runPostProcessingV2(
String filePath, {
Map<String, dynamic>? metadata,
@@ -2133,11 +2055,6 @@ class PlatformBridge {
});
}
static Future<List<Map<String, dynamic>>> getPostProcessingProviders() async {
final result = await _channel.invokeMethod('getPostProcessingProviders');
return _decodeMapListResult(result, 'getPostProcessingProviders');
}
static Future<void> initExtensionRepo(String cacheDir) async {
_log.d('initExtensionRepo: $cacheDir');
await _channel.invokeMethod('initExtensionRepo', {'cache_dir': cacheDir});