update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled

This commit is contained in:
2025-12-08 16:16:23 +01:00
commit c251f174ed
1349 changed files with 194301 additions and 0 deletions

145
plugins/discovery.go Normal file
View File

@@ -0,0 +1,145 @@
package plugins
import (
"fmt"
"os"
"path/filepath"
"github.com/navidrome/navidrome/plugins/schema"
)
// PluginDiscoveryEntry represents the result of plugin discovery
type PluginDiscoveryEntry struct {
ID string // Plugin ID (directory name)
Path string // Resolved plugin directory path
WasmPath string // Path to the WASM file
Manifest *schema.PluginManifest // Loaded manifest (nil if failed)
IsSymlink bool // Whether the plugin is a development symlink
Error error // Error encountered during discovery
}
// DiscoverPlugins scans the plugins directory and returns information about all discoverable plugins
// This shared function eliminates duplication between ScanPlugins and plugin list commands
func DiscoverPlugins(pluginsDir string) []PluginDiscoveryEntry {
var discoveries []PluginDiscoveryEntry
entries, err := os.ReadDir(pluginsDir)
if err != nil {
// Return a single entry with the error
return []PluginDiscoveryEntry{{
Error: fmt.Errorf("failed to read plugins directory %s: %w", pluginsDir, err),
}}
}
for _, entry := range entries {
name := entry.Name()
pluginPath := filepath.Join(pluginsDir, name)
// Skip hidden files
if name[0] == '.' {
continue
}
// Check if it's a directory or symlink
info, err := os.Lstat(pluginPath)
if err != nil {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
Error: fmt.Errorf("failed to stat entry %s: %w", pluginPath, err),
})
continue
}
isSymlink := info.Mode()&os.ModeSymlink != 0
isDir := info.IsDir()
// Skip if not a directory or symlink
if !isDir && !isSymlink {
continue
}
// Resolve symlinks
pluginDir := pluginPath
if isSymlink {
targetDir, err := os.Readlink(pluginPath)
if err != nil {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
IsSymlink: true,
Error: fmt.Errorf("failed to resolve symlink %s: %w", pluginPath, err),
})
continue
}
// If target is a relative path, make it absolute
if !filepath.IsAbs(targetDir) {
targetDir = filepath.Join(filepath.Dir(pluginPath), targetDir)
}
// Verify that the target is a directory
targetInfo, err := os.Stat(targetDir)
if err != nil {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
IsSymlink: true,
Error: fmt.Errorf("failed to stat symlink target %s: %w", targetDir, err),
})
continue
}
if !targetInfo.IsDir() {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
IsSymlink: true,
Error: fmt.Errorf("symlink target is not a directory: %s", targetDir),
})
continue
}
pluginDir = targetDir
}
// Check for WASM file
wasmPath := filepath.Join(pluginDir, "plugin.wasm")
if _, err := os.Stat(wasmPath); err != nil {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
Path: pluginDir,
Error: fmt.Errorf("no plugin.wasm found: %w", err),
})
continue
}
// Load manifest
manifest, err := LoadManifest(pluginDir)
if err != nil {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
Path: pluginDir,
Error: fmt.Errorf("failed to load manifest: %w", err),
})
continue
}
// Check for capabilities
if len(manifest.Capabilities) == 0 {
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
Path: pluginDir,
Error: fmt.Errorf("no capabilities found in manifest"),
})
continue
}
// Success!
discoveries = append(discoveries, PluginDiscoveryEntry{
ID: name,
Path: pluginDir,
WasmPath: wasmPath,
Manifest: manifest,
IsSymlink: isSymlink,
})
}
return discoveries
}