mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-04 01:58:36 +02:00
MatchesURL treated patterns as substrings of the whole URL, so "spotify.com" matched any URL embedding it in a query parameter and a hostile query string could route a link to the wrong handler. Patterns now anchor to the URL host (exact domain or subdomain, optional path prefix); "scheme:" patterns anchor to the URI front. With several matching handlers, FindURLHandler now breaks the tie via the user's metadata provider priority instead of Go's random map order.
325 lines
9.2 KiB
Go
325 lines
9.2 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
func (m *extensionManager) GetMetadataProviders() []*extensionProviderWrapper {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
var providers []*extensionProviderWrapper
|
|
for _, ext := range m.extensions {
|
|
if ext.Enabled && ext.Manifest.IsMetadataProvider() && ext.Error == "" {
|
|
providers = append(providers, newExtensionProviderWrapper(ext))
|
|
}
|
|
}
|
|
return providers
|
|
}
|
|
|
|
func (m *extensionManager) GetDownloadProviders() []*extensionProviderWrapper {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
var providers []*extensionProviderWrapper
|
|
for _, ext := range m.extensions {
|
|
if ext.Enabled && ext.Manifest.IsDownloadProvider() && ext.Error == "" {
|
|
providers = append(providers, newExtensionProviderWrapper(ext))
|
|
}
|
|
}
|
|
return providers
|
|
}
|
|
|
|
func metadataTrackDedupKey(track ExtTrackMetadata) string {
|
|
if isrc := strings.TrimSpace(track.ISRC); isrc != "" {
|
|
return "isrc:" + strings.ToUpper(isrc)
|
|
}
|
|
if spotifyID := strings.TrimSpace(track.SpotifyID); spotifyID != "" {
|
|
return "spotify:" + spotifyID
|
|
}
|
|
if providerID := strings.TrimSpace(track.ProviderID); providerID != "" && strings.TrimSpace(track.ID) != "" {
|
|
return providerID + ":" + strings.TrimSpace(track.ID)
|
|
}
|
|
return strings.TrimSpace(track.Name) + "|" + strings.TrimSpace(track.Artists)
|
|
}
|
|
|
|
func (m *extensionManager) SearchTracksWithMetadataProviders(query string, limit int, includeExtensions bool) ([]ExtTrackMetadata, error) {
|
|
return m.SearchTracksWithMetadataProvidersForItemID(query, limit, includeExtensions, "")
|
|
}
|
|
|
|
func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query string, limit int, includeExtensions bool, itemID string) ([]ExtTrackMetadata, error) {
|
|
priority := GetMetadataProviderPriority()
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
|
|
extensionProviders := make(map[string]*extensionProviderWrapper)
|
|
if includeExtensions {
|
|
for _, provider := range m.GetMetadataProviders() {
|
|
extensionProviders[provider.extension.ID] = provider
|
|
}
|
|
}
|
|
|
|
orderedProviderIDs := make([]string, 0, len(priority)+len(extensionProviders))
|
|
seenProviderIDs := make(map[string]struct{}, len(priority)+len(extensionProviders))
|
|
for _, providerID := range priority {
|
|
providerID = strings.TrimSpace(providerID)
|
|
if providerID == "" {
|
|
continue
|
|
}
|
|
orderedProviderIDs = append(orderedProviderIDs, providerID)
|
|
seenProviderIDs[providerID] = struct{}{}
|
|
}
|
|
if includeExtensions {
|
|
remainingIDs := make([]string, 0, len(extensionProviders))
|
|
for providerID := range extensionProviders {
|
|
if _, exists := seenProviderIDs[providerID]; exists {
|
|
continue
|
|
}
|
|
remainingIDs = append(remainingIDs, providerID)
|
|
}
|
|
sort.Strings(remainingIDs)
|
|
orderedProviderIDs = append(orderedProviderIDs, remainingIDs...)
|
|
}
|
|
|
|
tracks := make([]ExtTrackMetadata, 0, limit)
|
|
seenTracks := make(map[string]struct{})
|
|
for _, providerID := range orderedProviderIDs {
|
|
if isDownloadCancelled(itemID) {
|
|
return nil, ErrDownloadCancelled
|
|
}
|
|
|
|
if !includeExtensions {
|
|
continue
|
|
}
|
|
provider := extensionProviders[providerID]
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
result, err := provider.SearchTracksForItemID(query, limit, itemID)
|
|
providerTracks := []ExtTrackMetadata(nil)
|
|
if result != nil {
|
|
providerTracks = result.Tracks
|
|
}
|
|
|
|
if err != nil {
|
|
if errors.Is(err, ErrDownloadCancelled) {
|
|
return nil, ErrDownloadCancelled
|
|
}
|
|
GoLog("[MetadataSearch] Search error from %s: %v\n", providerID, err)
|
|
continue
|
|
}
|
|
|
|
for _, track := range providerTracks {
|
|
key := metadataTrackDedupKey(track)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if _, exists := seenTracks[key]; exists {
|
|
continue
|
|
}
|
|
seenTracks[key] = struct{}{}
|
|
tracks = append(tracks, track)
|
|
if len(tracks) >= limit {
|
|
return tracks, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return tracks, nil
|
|
}
|
|
|
|
// FindURLHandler returns the enabled handler matching the URL. When several
|
|
// extensions match (e.g. two Spotify handlers), the user's metadata provider
|
|
// priority breaks the tie deterministically instead of Go's random map
|
|
// iteration order.
|
|
func (m *extensionManager) FindURLHandler(url string) *extensionProviderWrapper {
|
|
m.mu.RLock()
|
|
matches := make([]*loadedExtension, 0, 2)
|
|
for _, ext := range m.extensions {
|
|
if ext.Enabled && ext.Manifest.MatchesURL(url) && ext.Error == "" {
|
|
matches = append(matches, ext)
|
|
}
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
if len(matches) > 1 {
|
|
rank := map[string]int{}
|
|
for i, id := range GetMetadataProviderPriority() {
|
|
rank[strings.ToLower(strings.TrimSpace(id))] = i
|
|
}
|
|
sort.SliceStable(matches, func(i, j int) bool {
|
|
ri, oki := rank[strings.ToLower(matches[i].ID)]
|
|
rj, okj := rank[strings.ToLower(matches[j].ID)]
|
|
switch {
|
|
case oki && okj:
|
|
return ri < rj
|
|
case oki:
|
|
return true
|
|
case okj:
|
|
return false
|
|
default:
|
|
return matches[i].ID < matches[j].ID
|
|
}
|
|
})
|
|
}
|
|
return newExtensionProviderWrapper(matches[0])
|
|
}
|
|
|
|
type ExtURLHandleResultWithExtID struct {
|
|
Result *ExtURLHandleResult
|
|
ExtensionID string
|
|
}
|
|
|
|
func (m *extensionManager) HandleURLWithExtension(url string) (*ExtURLHandleResultWithExtID, error) {
|
|
handler := m.FindURLHandler(url)
|
|
if handler == nil {
|
|
return nil, fmt.Errorf("no extension found to handle URL: %s", url)
|
|
}
|
|
|
|
result, err := handler.HandleURL(url)
|
|
if err != nil {
|
|
return &ExtURLHandleResultWithExtID{
|
|
Result: nil,
|
|
ExtensionID: handler.extension.ID,
|
|
}, err
|
|
}
|
|
|
|
return &ExtURLHandleResultWithExtID{
|
|
Result: result,
|
|
ExtensionID: handler.extension.ID,
|
|
}, nil
|
|
}
|
|
|
|
func (m *extensionManager) GetPostProcessingProviders() []*extensionProviderWrapper {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
var providers []*extensionProviderWrapper
|
|
for _, ext := range m.extensions {
|
|
if ext.Enabled && ext.Manifest.HasPostProcessing() && ext.Error == "" {
|
|
providers = append(providers, newExtensionProviderWrapper(ext))
|
|
}
|
|
}
|
|
return providers
|
|
}
|
|
|
|
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 := "[PostProcessV2]"
|
|
|
|
currentInput := input
|
|
for _, provider := range providers {
|
|
hooks := provider.extension.Manifest.GetPostProcessingHooks()
|
|
for _, hook := range hooks {
|
|
if !hook.DefaultEnabled {
|
|
continue
|
|
}
|
|
|
|
ext := strings.ToLower(filepath.Ext(currentInput.Path))
|
|
if ext == "" && currentInput.Name != "" {
|
|
ext = strings.ToLower(filepath.Ext(currentInput.Name))
|
|
}
|
|
if len(hook.SupportedFormats) > 0 && ext != "" {
|
|
supported := false
|
|
for _, format := range hook.SupportedFormats {
|
|
if "."+format == ext || format == ext[1:] {
|
|
supported = true
|
|
break
|
|
}
|
|
}
|
|
if !supported {
|
|
continue
|
|
}
|
|
}
|
|
|
|
GoLog("%s Running hook %s from %s on %s\n", logTag, hook.ID, provider.extension.ID, currentInput.Path)
|
|
|
|
result, err := provider.PostProcessV2(currentInput, metadata, hook.ID)
|
|
if err != nil {
|
|
GoLog("%s Hook %s failed: %v\n", logTag, hook.ID, err)
|
|
continue
|
|
}
|
|
if result.Success {
|
|
if err := validatePostProcessResult(provider.extension, currentInput, result); err != nil {
|
|
GoLog("%s Hook %s returned an unsafe result: %v\n", logTag, hook.ID, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if result.Success && result.NewFilePath != "" {
|
|
currentInput.Path = result.NewFilePath
|
|
if currentInput.Name == "" {
|
|
currentInput.Name = filepath.Base(result.NewFilePath)
|
|
}
|
|
}
|
|
if result.Success && result.NewFileURI != "" {
|
|
currentInput.URI = result.NewFileURI
|
|
}
|
|
}
|
|
}
|
|
|
|
return &PostProcessResult{Success: true, NewFilePath: currentInput.Path, NewFileURI: currentInput.URI}, nil
|
|
}
|
|
|
|
func validatePostProcessResult(ext *loadedExtension, input PostProcessInput, result *PostProcessResult) error {
|
|
if ext == nil || ext.Manifest == nil || result == nil {
|
|
return fmt.Errorf("invalid post-processing result")
|
|
}
|
|
if result.NewFileURI != "" && result.NewFileURI != input.URI {
|
|
return fmt.Errorf("an extension cannot replace the destination URI")
|
|
}
|
|
if result.NewFilePath == "" || filepath.Clean(result.NewFilePath) == filepath.Clean(input.Path) {
|
|
return nil
|
|
}
|
|
if !ext.Manifest.Permissions.File {
|
|
return fmt.Errorf("file permission is required to replace the processed file")
|
|
}
|
|
if !filepath.IsAbs(result.NewFilePath) {
|
|
return fmt.Errorf("replacement file path must be absolute")
|
|
}
|
|
if input.Path != "" && isPathWithinBase(filepath.Dir(input.Path), result.NewFilePath) {
|
|
return nil
|
|
}
|
|
if ext.DataDir != "" && isPathWithinBase(ext.DataDir, result.NewFilePath) {
|
|
return nil
|
|
}
|
|
allowedDownloadDirsMu.RLock()
|
|
defer allowedDownloadDirsMu.RUnlock()
|
|
for _, dir := range allowedDownloadDirs {
|
|
if isPathWithinBase(dir, result.NewFilePath) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("replacement file path is outside allowed directories")
|
|
}
|
|
|
|
func (m *extensionManager) GetLyricsProviders() []*extensionProviderWrapper {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
var providers []*extensionProviderWrapper
|
|
for _, ext := range m.extensions {
|
|
if ext.Enabled && ext.Manifest.IsLyricsProvider() && ext.Error == "" {
|
|
providers = append(providers, newExtensionProviderWrapper(ext))
|
|
}
|
|
}
|
|
|
|
sort.Slice(providers, func(i, j int) bool {
|
|
return providers[i].extension.ID < providers[j].extension.ID
|
|
})
|
|
|
|
return providers
|
|
}
|