diff --git a/Makefile b/Makefile index ae474eaf2..d6e8d449b 100644 --- a/Makefile +++ b/Makefile @@ -266,24 +266,6 @@ deprecated: @echo "WARNING: This target is deprecated and will be removed in future releases. Use 'make build' instead." .PHONY: deprecated -# Generate Go code from plugins/api/api.proto -plugin-gen: check_go_env ##@Development Generate Go code from plugins protobuf files - go generate ./plugins/... -.PHONY: plugin-gen - -plugin-examples: check_go_env ##@Development Build all example plugins - $(MAKE) -C plugins/examples clean all -.PHONY: plugin-examples - -plugin-clean: check_go_env ##@Development Clean all plugins - $(MAKE) -C plugins/examples clean - $(MAKE) -C plugins/testdata clean -.PHONY: plugin-clean - -plugin-tests: check_go_env ##@Development Build all test plugins - $(MAKE) -C plugins/testdata clean all -.PHONY: plugin-tests - .DEFAULT_GOAL := help HELP_FUN = \ diff --git a/cmd/plugin.go b/cmd/plugin.go deleted file mode 100644 index 0f3b66078..000000000 --- a/cmd/plugin.go +++ /dev/null @@ -1,716 +0,0 @@ -package cmd - -import ( - "cmp" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "text/tabwriter" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins" - "github.com/navidrome/navidrome/plugins/schema" - "github.com/navidrome/navidrome/utils" - "github.com/navidrome/navidrome/utils/slice" - "github.com/spf13/cobra" -) - -const ( - pluginPackageExtension = ".ndp" - pluginDirPermissions = 0700 - pluginFilePermissions = 0600 -) - -func init() { - pluginCmd := &cobra.Command{ - Use: "plugin", - Short: "Manage Navidrome plugins", - Long: "Commands for managing Navidrome plugins", - } - - listCmd := &cobra.Command{ - Use: "list", - Short: "List installed plugins", - Long: "List all installed plugins with their metadata", - Run: pluginList, - } - - infoCmd := &cobra.Command{ - Use: "info [pluginPackage|pluginName]", - Short: "Show details of a plugin", - Long: "Show detailed information about a plugin package (.ndp file) or an installed plugin", - Args: cobra.ExactArgs(1), - Run: pluginInfo, - } - - installCmd := &cobra.Command{ - Use: "install [pluginPackage]", - Short: "Install a plugin from a .ndp file", - Long: "Install a Navidrome Plugin Package (.ndp) file", - Args: cobra.ExactArgs(1), - Run: pluginInstall, - } - - removeCmd := &cobra.Command{ - Use: "remove [pluginName]", - Short: "Remove an installed plugin", - Long: "Remove a plugin by name", - Args: cobra.ExactArgs(1), - Run: pluginRemove, - } - - updateCmd := &cobra.Command{ - Use: "update [pluginPackage]", - Short: "Update an existing plugin", - Long: "Update an installed plugin with a new version from a .ndp file", - Args: cobra.ExactArgs(1), - Run: pluginUpdate, - } - - refreshCmd := &cobra.Command{ - Use: "refresh [pluginName]", - Short: "Reload a plugin without restarting Navidrome", - Long: "Reload and recompile a plugin without needing to restart Navidrome", - Args: cobra.ExactArgs(1), - Run: pluginRefresh, - } - - devCmd := &cobra.Command{ - Use: "dev [folder_path]", - Short: "Create symlink to development folder", - Long: "Create a symlink from a plugin development folder to the plugins directory for easier development", - Args: cobra.ExactArgs(1), - Run: pluginDev, - } - - pluginCmd.AddCommand(listCmd, infoCmd, installCmd, removeCmd, updateCmd, refreshCmd, devCmd) - rootCmd.AddCommand(pluginCmd) -} - -// Validation helpers - -func validatePluginPackageFile(path string) error { - if !utils.FileExists(path) { - return fmt.Errorf("plugin package not found: %s", path) - } - if filepath.Ext(path) != pluginPackageExtension { - return fmt.Errorf("not a valid plugin package: %s (expected %s extension)", path, pluginPackageExtension) - } - return nil -} - -func validatePluginDirectory(pluginsDir, pluginName string) (string, error) { - pluginDir := filepath.Join(pluginsDir, pluginName) - if !utils.FileExists(pluginDir) { - return "", fmt.Errorf("plugin not found: %s (path: %s)", pluginName, pluginDir) - } - return pluginDir, nil -} - -func resolvePluginPath(pluginDir string) (resolvedPath string, isSymlink bool, err error) { - // Check if it's a directory or a symlink - lstat, err := os.Lstat(pluginDir) - if err != nil { - return "", false, fmt.Errorf("failed to stat plugin: %w", err) - } - - isSymlink = lstat.Mode()&os.ModeSymlink != 0 - - if isSymlink { - // Resolve the symlink target - targetDir, err := os.Readlink(pluginDir) - if err != nil { - return "", true, fmt.Errorf("failed to resolve symlink: %w", err) - } - - // If target is a relative path, make it absolute - if !filepath.IsAbs(targetDir) { - targetDir = filepath.Join(filepath.Dir(pluginDir), targetDir) - } - - // Verify the target exists and is a directory - targetInfo, err := os.Stat(targetDir) - if err != nil { - return "", true, fmt.Errorf("failed to access symlink target %s: %w", targetDir, err) - } - - if !targetInfo.IsDir() { - return "", true, fmt.Errorf("symlink target is not a directory: %s", targetDir) - } - - return targetDir, true, nil - } else if !lstat.IsDir() { - return "", false, fmt.Errorf("not a valid plugin directory: %s", pluginDir) - } - - return pluginDir, false, nil -} - -// Package handling helpers - -func loadAndValidatePackage(ndpPath string) (*plugins.PluginPackage, error) { - if err := validatePluginPackageFile(ndpPath); err != nil { - return nil, err - } - - pkg, err := plugins.LoadPackage(ndpPath) - if err != nil { - return nil, fmt.Errorf("failed to load plugin package: %w", err) - } - - return pkg, nil -} - -func extractAndSetupPlugin(ndpPath, targetDir string) error { - if err := plugins.ExtractPackage(ndpPath, targetDir); err != nil { - return fmt.Errorf("failed to extract plugin package: %w", err) - } - - ensurePluginDirPermissions(targetDir) - return nil -} - -// Display helpers - -func displayPluginTableRow(w *tabwriter.Writer, discovery plugins.PluginDiscoveryEntry) { - if discovery.Error != nil { - // Handle global errors (like directory read failure) - if discovery.ID == "" { - log.Error("Failed to read plugins directory", "folder", conf.Server.Plugins.Folder, discovery.Error) - return - } - // Handle individual plugin errors - show them in the table - fmt.Fprintf(w, "%s\tERROR\tERROR\tERROR\tERROR\t%v\n", discovery.ID, discovery.Error) - return - } - - // Mark symlinks with an indicator - nameDisplay := discovery.Manifest.Name - if discovery.IsSymlink { - nameDisplay = nameDisplay + " (dev)" - } - - // Convert capabilities to strings - capabilities := slice.Map(discovery.Manifest.Capabilities, func(cap schema.PluginManifestCapabilitiesElem) string { - return string(cap) - }) - - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", - discovery.ID, - nameDisplay, - cmp.Or(discovery.Manifest.Author, "-"), - cmp.Or(discovery.Manifest.Version, "-"), - strings.Join(capabilities, ", "), - cmp.Or(discovery.Manifest.Description, "-")) -} - -func displayTypedPermissions(permissions schema.PluginManifestPermissions, indent string) { - if permissions.Http != nil { - fmt.Printf("%shttp:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Http.Reason) - fmt.Printf("%s Allow Local Network: %t\n", indent, permissions.Http.AllowLocalNetwork) - fmt.Printf("%s Allowed URLs:\n", indent) - for urlPattern, methodEnums := range permissions.Http.AllowedUrls { - methods := make([]string, len(methodEnums)) - for i, methodEnum := range methodEnums { - methods[i] = string(methodEnum) - } - fmt.Printf("%s %s: [%s]\n", indent, urlPattern, strings.Join(methods, ", ")) - } - fmt.Println() - } - - if permissions.Config != nil { - fmt.Printf("%sconfig:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Config.Reason) - fmt.Println() - } - - if permissions.Scheduler != nil { - fmt.Printf("%sscheduler:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Scheduler.Reason) - fmt.Println() - } - - if permissions.Websocket != nil { - fmt.Printf("%swebsocket:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Websocket.Reason) - fmt.Printf("%s Allow Local Network: %t\n", indent, permissions.Websocket.AllowLocalNetwork) - fmt.Printf("%s Allowed URLs: [%s]\n", indent, strings.Join(permissions.Websocket.AllowedUrls, ", ")) - fmt.Println() - } - - if permissions.Cache != nil { - fmt.Printf("%scache:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Cache.Reason) - fmt.Println() - } - - if permissions.Artwork != nil { - fmt.Printf("%sartwork:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Artwork.Reason) - fmt.Println() - } - - if permissions.Subsonicapi != nil { - allowedUsers := "All Users" - if len(permissions.Subsonicapi.AllowedUsernames) > 0 { - allowedUsers = strings.Join(permissions.Subsonicapi.AllowedUsernames, ", ") - } - fmt.Printf("%ssubsonicapi:\n", indent) - fmt.Printf("%s Reason: %s\n", indent, permissions.Subsonicapi.Reason) - fmt.Printf("%s Allow Admins: %t\n", indent, permissions.Subsonicapi.AllowAdmins) - fmt.Printf("%s Allowed Usernames: [%s]\n", indent, allowedUsers) - fmt.Println() - } -} - -func displayPluginDetails(manifest *schema.PluginManifest, fileInfo *pluginFileInfo, permInfo *pluginPermissionInfo) { - fmt.Println("\nPlugin Information:") - fmt.Printf(" Name: %s\n", manifest.Name) - fmt.Printf(" Author: %s\n", manifest.Author) - fmt.Printf(" Version: %s\n", manifest.Version) - fmt.Printf(" Description: %s\n", manifest.Description) - - fmt.Print(" Capabilities: ") - capabilities := make([]string, len(manifest.Capabilities)) - for i, cap := range manifest.Capabilities { - capabilities[i] = string(cap) - } - fmt.Print(strings.Join(capabilities, ", ")) - fmt.Println() - - // Display manifest permissions using the typed permissions - fmt.Println(" Required Permissions:") - displayTypedPermissions(manifest.Permissions, " ") - - // Print file information if available - if fileInfo != nil { - fmt.Println("Package Information:") - fmt.Printf(" File: %s\n", fileInfo.path) - fmt.Printf(" Size: %d bytes (%.2f KB)\n", fileInfo.size, float64(fileInfo.size)/1024) - fmt.Printf(" SHA-256: %s\n", fileInfo.hash) - fmt.Printf(" Modified: %s\n", fileInfo.modTime.Format(time.RFC3339)) - } - - // Print file permissions information if available - if permInfo != nil { - fmt.Println("File Permissions:") - fmt.Printf(" Plugin Directory: %s (%s)\n", permInfo.dirPath, permInfo.dirMode) - if permInfo.isSymlink { - fmt.Printf(" Symlink Target: %s (%s)\n", permInfo.targetPath, permInfo.targetMode) - } - fmt.Printf(" Manifest File: %s\n", permInfo.manifestMode) - if permInfo.wasmMode != "" { - fmt.Printf(" WASM File: %s\n", permInfo.wasmMode) - } - } -} - -type pluginFileInfo struct { - path string - size int64 - hash string - modTime time.Time -} - -type pluginPermissionInfo struct { - dirPath string - dirMode string - isSymlink bool - targetPath string - targetMode string - manifestMode string - wasmMode string -} - -func getFileInfo(path string) *pluginFileInfo { - fileInfo, err := os.Stat(path) - if err != nil { - log.Error("Failed to get file information", err) - return nil - } - - return &pluginFileInfo{ - path: path, - size: fileInfo.Size(), - hash: calculateSHA256(path), - modTime: fileInfo.ModTime(), - } -} - -func getPermissionInfo(pluginDir string) *pluginPermissionInfo { - // Get plugin directory permissions - dirInfo, err := os.Lstat(pluginDir) - if err != nil { - log.Error("Failed to get plugin directory permissions", err) - return nil - } - - permInfo := &pluginPermissionInfo{ - dirPath: pluginDir, - dirMode: dirInfo.Mode().String(), - } - - // Check if it's a symlink - if dirInfo.Mode()&os.ModeSymlink != 0 { - permInfo.isSymlink = true - - // Get target path and permissions - targetPath, err := os.Readlink(pluginDir) - if err == nil { - if !filepath.IsAbs(targetPath) { - targetPath = filepath.Join(filepath.Dir(pluginDir), targetPath) - } - permInfo.targetPath = targetPath - - if targetInfo, err := os.Stat(targetPath); err == nil { - permInfo.targetMode = targetInfo.Mode().String() - } - } - } - - // Get manifest file permissions - manifestPath := filepath.Join(pluginDir, "manifest.json") - if manifestInfo, err := os.Stat(manifestPath); err == nil { - permInfo.manifestMode = manifestInfo.Mode().String() - } - - // Get WASM file permissions (look for .wasm files) - entries, err := os.ReadDir(pluginDir) - if err == nil { - for _, entry := range entries { - if filepath.Ext(entry.Name()) == ".wasm" { - wasmPath := filepath.Join(pluginDir, entry.Name()) - if wasmInfo, err := os.Stat(wasmPath); err == nil { - permInfo.wasmMode = wasmInfo.Mode().String() - break // Just show the first WASM file found - } - } - } - } - - return permInfo -} - -// Command implementations - -func pluginList(cmd *cobra.Command, args []string) { - discoveries := plugins.DiscoverPlugins(conf.Server.Plugins.Folder) - - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "ID\tNAME\tAUTHOR\tVERSION\tCAPABILITIES\tDESCRIPTION") - - for _, discovery := range discoveries { - displayPluginTableRow(w, discovery) - } - w.Flush() -} - -func pluginInfo(cmd *cobra.Command, args []string) { - path := args[0] - pluginsDir := conf.Server.Plugins.Folder - - var manifest *schema.PluginManifest - var fileInfo *pluginFileInfo - var permInfo *pluginPermissionInfo - - if filepath.Ext(path) == pluginPackageExtension { - // It's a package file - pkg, err := loadAndValidatePackage(path) - if err != nil { - log.Fatal("Failed to load plugin package", err) - } - manifest = pkg.Manifest - fileInfo = getFileInfo(path) - // No permission info for package files - } else { - // It's a plugin name - pluginDir, err := validatePluginDirectory(pluginsDir, path) - if err != nil { - log.Fatal("Plugin validation failed", err) - } - - manifest, err = plugins.LoadManifest(pluginDir) - if err != nil { - log.Fatal("Failed to load plugin manifest", err) - } - - // Get permission info for installed plugins - permInfo = getPermissionInfo(pluginDir) - } - - displayPluginDetails(manifest, fileInfo, permInfo) -} - -func pluginInstall(cmd *cobra.Command, args []string) { - ndpPath := args[0] - pluginsDir := conf.Server.Plugins.Folder - - pkg, err := loadAndValidatePackage(ndpPath) - if err != nil { - log.Fatal("Package validation failed", err) - } - - // Create target directory based on plugin name - targetDir := filepath.Join(pluginsDir, pkg.Manifest.Name) - - // Check if plugin already exists - if utils.FileExists(targetDir) { - log.Fatal("Plugin already installed", "name", pkg.Manifest.Name, "path", targetDir, - "use", "navidrome plugin update") - } - - if err := extractAndSetupPlugin(ndpPath, targetDir); err != nil { - log.Fatal("Plugin installation failed", err) - } - - fmt.Printf("Plugin '%s' v%s installed successfully\n", pkg.Manifest.Name, pkg.Manifest.Version) -} - -func pluginRemove(cmd *cobra.Command, args []string) { - pluginName := args[0] - pluginsDir := conf.Server.Plugins.Folder - - pluginDir, err := validatePluginDirectory(pluginsDir, pluginName) - if err != nil { - log.Fatal("Plugin validation failed", err) - } - - _, isSymlink, err := resolvePluginPath(pluginDir) - if err != nil { - log.Fatal("Failed to resolve plugin path", err) - } - - if isSymlink { - // For symlinked plugins (dev mode), just remove the symlink - if err := os.Remove(pluginDir); err != nil { - log.Fatal("Failed to remove plugin symlink", "name", pluginName, err) - } - fmt.Printf("Development plugin symlink '%s' removed successfully (target directory preserved)\n", pluginName) - } else { - // For regular plugins, remove the entire directory - if err := os.RemoveAll(pluginDir); err != nil { - log.Fatal("Failed to remove plugin directory", "name", pluginName, err) - } - fmt.Printf("Plugin '%s' removed successfully\n", pluginName) - } -} - -func pluginUpdate(cmd *cobra.Command, args []string) { - ndpPath := args[0] - pluginsDir := conf.Server.Plugins.Folder - - pkg, err := loadAndValidatePackage(ndpPath) - if err != nil { - log.Fatal("Package validation failed", err) - } - - // Check if plugin exists - targetDir := filepath.Join(pluginsDir, pkg.Manifest.Name) - if !utils.FileExists(targetDir) { - log.Fatal("Plugin not found", "name", pkg.Manifest.Name, "path", targetDir, - "use", "navidrome plugin install") - } - - // Create a backup of the existing plugin - backupDir := targetDir + ".bak." + time.Now().Format("20060102150405") - if err := os.Rename(targetDir, backupDir); err != nil { - log.Fatal("Failed to backup existing plugin", err) - } - - // Extract the new package - if err := extractAndSetupPlugin(ndpPath, targetDir); err != nil { - // Restore backup if extraction failed - os.RemoveAll(targetDir) - _ = os.Rename(backupDir, targetDir) // Ignore error as we're already in a fatal path - log.Fatal("Plugin update failed", err) - } - - // Remove the backup - os.RemoveAll(backupDir) - - fmt.Printf("Plugin '%s' updated to v%s successfully\n", pkg.Manifest.Name, pkg.Manifest.Version) -} - -func pluginRefresh(cmd *cobra.Command, args []string) { - pluginName := args[0] - pluginsDir := conf.Server.Plugins.Folder - - pluginDir, err := validatePluginDirectory(pluginsDir, pluginName) - if err != nil { - log.Fatal("Plugin validation failed", err) - } - - resolvedPath, isSymlink, err := resolvePluginPath(pluginDir) - if err != nil { - log.Fatal("Failed to resolve plugin path", err) - } - - if isSymlink { - log.Debug("Processing symlinked plugin", "name", pluginName, "link", pluginDir, "target", resolvedPath) - } - - fmt.Printf("Refreshing plugin '%s'...\n", pluginName) - - // Get the plugin manager and refresh - mgr := GetPluginManager(cmd.Context()) - log.Debug("Scanning plugins directory", "path", pluginsDir) - mgr.ScanPlugins() - - log.Info("Waiting for plugin compilation to complete", "name", pluginName) - - // Wait for compilation to complete - if err := mgr.EnsureCompiled(pluginName); err != nil { - log.Fatal("Failed to compile refreshed plugin", "name", pluginName, err) - } - - log.Info("Plugin compilation completed successfully", "name", pluginName) - fmt.Printf("Plugin '%s' refreshed successfully\n", pluginName) -} - -func pluginDev(cmd *cobra.Command, args []string) { - sourcePath, err := filepath.Abs(args[0]) - if err != nil { - log.Fatal("Invalid path", "path", args[0], err) - } - pluginsDir := conf.Server.Plugins.Folder - - // Validate source directory and manifest - if err := validateDevSource(sourcePath); err != nil { - log.Fatal("Source validation failed", err) - } - - // Load manifest to get plugin name - manifest, err := plugins.LoadManifest(sourcePath) - if err != nil { - log.Fatal("Failed to load plugin manifest", "path", filepath.Join(sourcePath, "manifest.json"), err) - } - - pluginName := cmp.Or(manifest.Name, filepath.Base(sourcePath)) - targetPath := filepath.Join(pluginsDir, pluginName) - - // Handle existing target - if err := handleExistingTarget(targetPath, sourcePath); err != nil { - log.Fatal("Failed to handle existing target", err) - } - - // Create target directory if needed - if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { - log.Fatal("Failed to create plugins directory", "path", filepath.Dir(targetPath), err) - } - - // Create the symlink - if err := os.Symlink(sourcePath, targetPath); err != nil { - log.Fatal("Failed to create symlink", "source", sourcePath, "target", targetPath, err) - } - - fmt.Printf("Development symlink created: '%s' -> '%s'\n", targetPath, sourcePath) - fmt.Println("Plugin can be refreshed with: navidrome plugin refresh", pluginName) -} - -// Utility functions - -func validateDevSource(sourcePath string) error { - sourceInfo, err := os.Stat(sourcePath) - if err != nil { - return fmt.Errorf("source folder not found: %s (%w)", sourcePath, err) - } - if !sourceInfo.IsDir() { - return fmt.Errorf("source path is not a directory: %s", sourcePath) - } - - manifestPath := filepath.Join(sourcePath, "manifest.json") - if !utils.FileExists(manifestPath) { - return fmt.Errorf("source folder missing manifest.json: %s", sourcePath) - } - - return nil -} - -func handleExistingTarget(targetPath, sourcePath string) error { - if !utils.FileExists(targetPath) { - return nil // Nothing to handle - } - - // Check if it's already a symlink to our source - existingLink, err := os.Readlink(targetPath) - if err == nil && existingLink == sourcePath { - fmt.Printf("Symlink already exists and points to the correct source\n") - return fmt.Errorf("symlink already exists") // This will cause early return in caller - } - - // Handle case where target exists but is not a symlink to our source - fmt.Printf("Target path '%s' already exists.\n", targetPath) - fmt.Print("Do you want to replace it? (y/N): ") - var response string - _, err = fmt.Scanln(&response) - if err != nil || strings.ToLower(response) != "y" { - if err != nil { - log.Debug("Error reading input, assuming 'no'", err) - } - return fmt.Errorf("operation canceled") - } - - // Remove existing target - if err := os.RemoveAll(targetPath); err != nil { - return fmt.Errorf("failed to remove existing target %s: %w", targetPath, err) - } - - return nil -} - -func ensurePluginDirPermissions(dir string) { - if err := os.Chmod(dir, pluginDirPermissions); err != nil { - log.Error("Failed to set plugin directory permissions", "dir", dir, err) - } - - // Apply permissions to all files in the directory - entries, err := os.ReadDir(dir) - if err != nil { - log.Error("Failed to read plugin directory", "dir", dir, err) - return - } - - for _, entry := range entries { - path := filepath.Join(dir, entry.Name()) - info, err := os.Stat(path) - if err != nil { - log.Error("Failed to stat file", "path", path, err) - continue - } - - mode := os.FileMode(pluginFilePermissions) // Files - if info.IsDir() { - mode = os.FileMode(pluginDirPermissions) // Directories - ensurePluginDirPermissions(path) // Recursive - } - - if err := os.Chmod(path, mode); err != nil { - log.Error("Failed to set file permissions", "path", path, err) - } - } -} - -func calculateSHA256(filePath string) string { - file, err := os.Open(filePath) - if err != nil { - log.Error("Failed to open file for hashing", err) - return "N/A" - } - defer file.Close() - - hasher := sha256.New() - if _, err := io.Copy(hasher, file); err != nil { - log.Error("Failed to calculate hash", err) - return "N/A" - } - - return hex.EncodeToString(hasher.Sum(nil)) -} diff --git a/cmd/plugin_test.go b/cmd/plugin_test.go deleted file mode 100644 index 3a4aefa88..000000000 --- a/cmd/plugin_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package cmd - -import ( - "io" - "os" - "path/filepath" - "strings" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/conf/configtest" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" -) - -var _ = Describe("Plugin CLI Commands", func() { - var tempDir string - var cmd *cobra.Command - var stdOut *os.File - var origStdout *os.File - var outReader *os.File - - // Helper to create a test plugin with the given name and details - createTestPlugin := func(name, author, version string, capabilities []string) string { - pluginDir := filepath.Join(tempDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Create a properly formatted capabilities JSON array - capabilitiesJSON := `"` + strings.Join(capabilities, `", "`) + `"` - - manifest := `{ - "name": "` + name + `", - "author": "` + author + `", - "version": "` + version + `", - "description": "Plugin for testing", - "website": "https://test.navidrome.org/` + name + `", - "capabilities": [` + capabilitiesJSON + `], - "permissions": {} - }` - - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - - // Create a dummy WASM file - wasmContent := []byte("dummy wasm content for testing") - Expect(os.WriteFile(filepath.Join(pluginDir, "plugin.wasm"), wasmContent, 0600)).To(Succeed()) - - return pluginDir - } - - // Helper to execute a command and return captured output - captureOutput := func(reader io.Reader) string { - stdOut.Close() - outputBytes, err := io.ReadAll(reader) - Expect(err).NotTo(HaveOccurred()) - return string(outputBytes) - } - - BeforeEach(func() { - DeferCleanup(configtest.SetupConfig()) - tempDir = GinkgoT().TempDir() - - // Setup config - conf.Server.Plugins.Enabled = true - conf.Server.Plugins.Folder = tempDir - - // Create a command for testing - cmd = &cobra.Command{Use: "test"} - - // Setup stdout capture - origStdout = os.Stdout - var err error - outReader, stdOut, err = os.Pipe() - Expect(err).NotTo(HaveOccurred()) - os.Stdout = stdOut - - DeferCleanup(func() { - os.Stdout = origStdout - }) - }) - - AfterEach(func() { - os.Stdout = origStdout - if stdOut != nil { - stdOut.Close() - } - if outReader != nil { - outReader.Close() - } - }) - - Describe("Plugin list command", func() { - It("should list installed plugins", func() { - // Create test plugins - createTestPlugin("plugin1", "Test Author", "1.0.0", []string{"MetadataAgent"}) - createTestPlugin("plugin2", "Another Author", "2.1.0", []string{"Scrobbler"}) - - // Execute command - pluginList(cmd, []string{}) - - // Verify output - output := captureOutput(outReader) - - Expect(output).To(ContainSubstring("plugin1")) - Expect(output).To(ContainSubstring("Test Author")) - Expect(output).To(ContainSubstring("1.0.0")) - Expect(output).To(ContainSubstring("MetadataAgent")) - - Expect(output).To(ContainSubstring("plugin2")) - Expect(output).To(ContainSubstring("Another Author")) - Expect(output).To(ContainSubstring("2.1.0")) - Expect(output).To(ContainSubstring("Scrobbler")) - }) - }) - - Describe("Plugin info command", func() { - It("should display information about an installed plugin", func() { - // Create test plugin with multiple capabilities - createTestPlugin("test-plugin", "Test Author", "1.0.0", - []string{"MetadataAgent", "Scrobbler"}) - - // Execute command - pluginInfo(cmd, []string{"test-plugin"}) - - // Verify output - output := captureOutput(outReader) - - Expect(output).To(ContainSubstring("Name: test-plugin")) - Expect(output).To(ContainSubstring("Author: Test Author")) - Expect(output).To(ContainSubstring("Version: 1.0.0")) - Expect(output).To(ContainSubstring("Description: Plugin for testing")) - Expect(output).To(ContainSubstring("Capabilities: MetadataAgent, Scrobbler")) - }) - }) - - Describe("Plugin remove command", func() { - It("should remove a regular plugin directory", func() { - // Create test plugin - pluginDir := createTestPlugin("regular-plugin", "Test Author", "1.0.0", - []string{"MetadataAgent"}) - - // Execute command - pluginRemove(cmd, []string{"regular-plugin"}) - - // Verify output - output := captureOutput(outReader) - Expect(output).To(ContainSubstring("Plugin 'regular-plugin' removed successfully")) - - // Verify directory is actually removed - _, err := os.Stat(pluginDir) - Expect(os.IsNotExist(err)).To(BeTrue()) - }) - - It("should remove only the symlink for a development plugin", func() { - // Create a real source directory - sourceDir := filepath.Join(GinkgoT().TempDir(), "dev-plugin-source") - Expect(os.MkdirAll(sourceDir, 0755)).To(Succeed()) - - manifest := `{ - "name": "dev-plugin", - "author": "Dev Author", - "version": "0.1.0", - "description": "Development plugin for testing", - "website": "https://test.navidrome.org/dev-plugin", - "capabilities": ["Scrobbler"], - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(sourceDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - - // Create a dummy WASM file - wasmContent := []byte("dummy wasm content for testing") - Expect(os.WriteFile(filepath.Join(sourceDir, "plugin.wasm"), wasmContent, 0600)).To(Succeed()) - - // Create a symlink in the plugins directory - symlinkPath := filepath.Join(tempDir, "dev-plugin") - Expect(os.Symlink(sourceDir, symlinkPath)).To(Succeed()) - - // Execute command - pluginRemove(cmd, []string{"dev-plugin"}) - - // Verify output - output := captureOutput(outReader) - Expect(output).To(ContainSubstring("Development plugin symlink 'dev-plugin' removed successfully")) - Expect(output).To(ContainSubstring("target directory preserved")) - - // Verify the symlink is removed but source directory exists - _, err := os.Lstat(symlinkPath) - Expect(os.IsNotExist(err)).To(BeTrue()) - - _, err = os.Stat(sourceDir) - Expect(err).NotTo(HaveOccurred()) - }) - }) -}) diff --git a/cmd/root.go b/cmd/root.go index 5e91ecd5f..eafa5d934 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -327,18 +327,10 @@ func startPlaybackServer(ctx context.Context) func() error { } } +// TODO(PLUGINS): Implement startPluginManager with new plugin system // startPluginManager starts the plugin manager, if configured. -func startPluginManager(ctx context.Context) func() error { +func startPluginManager(_ context.Context) func() error { return func() error { - if !conf.Server.Plugins.Enabled { - log.Debug("Plugins are DISABLED") - return nil - } - log.Info(ctx, "Starting plugin manager") - // Get the manager instance and scan for plugins - manager := GetPluginManager(ctx) - manager.ScanPlugins() - return nil } } diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index d7b6a3ad2..364f7cce2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -22,7 +22,6 @@ import ( "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" - "github.com/navidrome/navidrome/plugins" "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server/events" @@ -47,9 +46,7 @@ func CreateServer() *server.Server { sqlDB := db.Db() dataStore := persistence.New(sqlDB) broker := events.GetBroker() - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - insights := metrics.GetInstance(dataStore, manager) + insights := metrics.GetInstance(dataStore) serverServer := server.New(dataStore, broker, insights) return serverServer } @@ -59,16 +56,16 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { dataStore := persistence.New(sqlDB) share := core.NewShare(dataStore) playlists := core.NewPlaylists(dataStore) - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - insights := metrics.GetInstance(dataStore, manager) + insights := metrics.GetInstance(dataStore) fileCache := artwork.GetImageCache() fFmpeg := ffmpeg.New() - agentsAgents := agents.GetAgents(dataStore, manager) + noopPluginLoader := core.GetNoopPluginLoader() + agentsAgents := agents.GetAgents(dataStore, noopPluginLoader) provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() + metricsMetrics := metrics.GetPrometheusInstance(dataStore) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) watcher := scanner.GetWatcher(dataStore, modelScanner) library := core.NewLibrary(dataStore, modelScanner, watcher, broker) @@ -82,9 +79,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router { dataStore := persistence.New(sqlDB) fileCache := artwork.GetImageCache() fFmpeg := ffmpeg.New() - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - agentsAgents := agents.GetAgents(dataStore, manager) + noopPluginLoader := core.GetNoopPluginLoader() + agentsAgents := agents.GetAgents(dataStore, noopPluginLoader) provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) transcodingCache := core.GetTranscodingCache() @@ -95,8 +91,9 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router { cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) + metricsMetrics := metrics.GetPrometheusInstance(dataStore) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) - playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager) + playTracker := scrobbler.GetPlayTracker(dataStore, broker, noopPluginLoader) playbackServer := playback.GetInstance(dataStore) router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlists, playTracker, share, playbackServer, metricsMetrics) return router @@ -107,9 +104,8 @@ func CreatePublicRouter() *public.Router { dataStore := persistence.New(sqlDB) fileCache := artwork.GetImageCache() fFmpeg := ffmpeg.New() - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - agentsAgents := agents.GetAgents(dataStore, manager) + noopPluginLoader := core.GetNoopPluginLoader() + agentsAgents := agents.GetAgents(dataStore, noopPluginLoader) provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) transcodingCache := core.GetTranscodingCache() @@ -137,9 +133,7 @@ func CreateListenBrainzRouter() *listenbrainz.Router { func CreateInsights() metrics.Insights { sqlDB := db.Db() dataStore := persistence.New(sqlDB) - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - insights := metrics.GetInstance(dataStore, manager) + insights := metrics.GetInstance(dataStore) return insights } @@ -155,14 +149,14 @@ func CreateScanner(ctx context.Context) model.Scanner { dataStore := persistence.New(sqlDB) fileCache := artwork.GetImageCache() fFmpeg := ffmpeg.New() - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - agentsAgents := agents.GetAgents(dataStore, manager) + noopPluginLoader := core.GetNoopPluginLoader() + agentsAgents := agents.GetAgents(dataStore, noopPluginLoader) provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) + metricsMetrics := metrics.GetPrometheusInstance(dataStore) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) return modelScanner } @@ -172,14 +166,14 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher { dataStore := persistence.New(sqlDB) fileCache := artwork.GetImageCache() fFmpeg := ffmpeg.New() - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - agentsAgents := agents.GetAgents(dataStore, manager) + noopPluginLoader := core.GetNoopPluginLoader() + agentsAgents := agents.GetAgents(dataStore, noopPluginLoader) provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) + metricsMetrics := metrics.GetPrometheusInstance(dataStore) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) watcher := scanner.GetWatcher(dataStore, modelScanner) return watcher @@ -192,20 +186,6 @@ func GetPlaybackServer() playback.PlaybackServer { return playbackServer } -func getPluginManager() plugins.Manager { - sqlDB := db.Db() - dataStore := persistence.New(sqlDB) - metricsMetrics := metrics.GetPrometheusInstance(dataStore) - manager := plugins.GetManager(dataStore, metricsMetrics) - return manager -} - // wire_injectors.go: -var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, plugins.GetManager, metrics.GetPrometheusInstance, db.Db, wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), wire.Bind(new(metrics.PluginLoader), new(plugins.Manager)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) - -func GetPluginManager(ctx context.Context) plugins.Manager { - manager := getPluginManager() - manager.SetSubsonicRouter(CreateSubsonicAPIRouter(ctx)) - return manager -} +var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, core.GetNoopPluginLoader, wire.Bind(new(agents.PluginLoader), new(*core.NoopPluginLoader)), wire.Bind(new(scrobbler.PluginLoader), new(*core.NoopPluginLoader)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index 595d406b9..8a8a7ed2e 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -17,7 +17,6 @@ import ( "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" - "github.com/navidrome/navidrome/plugins" "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server/events" @@ -39,12 +38,12 @@ var allProviders = wire.NewSet( events.GetBroker, scanner.New, scanner.GetWatcher, - plugins.GetManager, metrics.GetPrometheusInstance, db.Db, - wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), - wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), - wire.Bind(new(metrics.PluginLoader), new(plugins.Manager)), + // TODO(PLUGINS): Replace NoopPluginLoader with actual plugin manager + core.GetNoopPluginLoader, + wire.Bind(new(agents.PluginLoader), new(*core.NoopPluginLoader)), + wire.Bind(new(scrobbler.PluginLoader), new(*core.NoopPluginLoader)), wire.Bind(new(core.Watcher), new(scanner.Watcher)), ) @@ -119,15 +118,3 @@ func GetPlaybackServer() playback.PlaybackServer { allProviders, )) } - -func getPluginManager() plugins.Manager { - panic(wire.Build( - allProviders, - )) -} - -func GetPluginManager(ctx context.Context) plugins.Manager { - manager := getPluginManager() - manager.SetSubsonicRouter(CreateSubsonicAPIRouter(ctx)) - return manager -} diff --git a/core/agents/agents.go b/core/agents/agents.go index cb10d2c4c..46f1a26ce 100644 --- a/core/agents/agents.go +++ b/core/agents/agents.go @@ -14,6 +14,7 @@ import ( "github.com/navidrome/navidrome/utils/singleton" ) +// TODO(PLUGINS): Implement PluginLoader with new plugin system // PluginLoader defines an interface for loading plugins type PluginLoader interface { // PluginNames returns the names of all plugins that implement a particular service diff --git a/core/metrics/insights.go b/core/metrics/insights.go index 411bc9ac1..af0bbcb50 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -23,7 +23,6 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" - "github.com/navidrome/navidrome/plugins/schema" "github.com/navidrome/navidrome/utils/singleton" ) @@ -37,18 +36,12 @@ var ( ) type insightsCollector struct { - ds model.DataStore - pluginLoader PluginLoader - lastRun atomic.Int64 - lastStatus atomic.Bool + ds model.DataStore + lastRun atomic.Int64 + lastStatus atomic.Bool } -// PluginLoader defines an interface for loading plugins -type PluginLoader interface { - PluginList() map[string]schema.PluginManifest -} - -func GetInstance(ds model.DataStore, pluginLoader PluginLoader) Insights { +func GetInstance(ds model.DataStore) Insights { return singleton.GetInstance(func() *insightsCollector { id, err := ds.Property(context.TODO()).Get(consts.InsightsIDKey) if err != nil { @@ -60,7 +53,7 @@ func GetInstance(ds model.DataStore, pluginLoader PluginLoader) Insights { } } insightsID = id - return &insightsCollector{ds: ds, pluginLoader: pluginLoader} + return &insightsCollector{ds: ds} }) } @@ -320,11 +313,6 @@ func (c *insightsCollector) hasSmartPlaylists(ctx context.Context) (bool, error) // collectPlugins collects information about installed plugins func (c *insightsCollector) collectPlugins(_ context.Context) map[string]insights.PluginInfo { plugins := make(map[string]insights.PluginInfo) - for id, manifest := range c.pluginLoader.PluginList() { - plugins[id] = insights.PluginInfo{ - Name: manifest.Name, - Version: manifest.Version, - } - } + // TODO(PLUGINS): Get the list from the new plugin system return plugins } diff --git a/core/noop_plugin_loader.go b/core/noop_plugin_loader.go new file mode 100644 index 000000000..f447006f7 --- /dev/null +++ b/core/noop_plugin_loader.go @@ -0,0 +1,38 @@ +package core + +import ( + "github.com/navidrome/navidrome/core/agents" + "github.com/navidrome/navidrome/core/scrobbler" +) + +// TODO(PLUGINS): Remove NoopPluginLoader when real plugin system is implemented + +// NoopPluginLoader is a stub implementation of plugin loaders that does nothing. +// This is used as a placeholder until the new plugin system is implemented. +type NoopPluginLoader struct{} + +// GetNoopPluginLoader returns a singleton noop plugin loader instance. +func GetNoopPluginLoader() *NoopPluginLoader { + return &NoopPluginLoader{} +} + +// PluginNames returns an empty slice (no plugins available) +func (n *NoopPluginLoader) PluginNames(_ string) []string { + return nil +} + +// LoadMediaAgent returns false (no plugin available) +func (n *NoopPluginLoader) LoadMediaAgent(_ string) (agents.Interface, bool) { + return nil, false +} + +// LoadScrobbler returns false (no plugin available) +func (n *NoopPluginLoader) LoadScrobbler(_ string) (scrobbler.Scrobbler, bool) { + return nil, false +} + +// Verify interface implementations at compile time +var ( + _ agents.PluginLoader = (*NoopPluginLoader)(nil) + _ scrobbler.PluginLoader = (*NoopPluginLoader)(nil) +) diff --git a/core/scrobbler/play_tracker.go b/core/scrobbler/play_tracker.go index 49c1dd87b..bc947ff74 100644 --- a/core/scrobbler/play_tracker.go +++ b/core/scrobbler/play_tracker.go @@ -44,6 +44,7 @@ type PlayTracker interface { Submit(ctx context.Context, submissions []Submission) error } +// TODO(PLUGINS): Implement PluginLoader with new plugin system // PluginLoader is a minimal interface for plugin manager usage in PlayTracker // (avoids import cycles) type PluginLoader interface { @@ -129,6 +130,7 @@ func pluginNamesMatchScrobblers(pluginNames []string, scrobblers map[string]Scro return true } +// TODO(PLUGINS): Implement refreshPluginScrobblers with new plugin system // refreshPluginScrobblers updates the pluginScrobblers map to match the current set of plugin scrobblers func (p *playTracker) refreshPluginScrobblers() { p.mu.Lock() diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 100230cbf..000000000 --- a/plugins/README.md +++ /dev/null @@ -1,1760 +0,0 @@ -# Navidrome Plugin System - -## Overview - -Navidrome's plugin system is a WebAssembly (WASM) based extension mechanism that enables developers to expand Navidrome's functionality without modifying the core codebase. The plugin system supports several capabilities that can be implemented by plugins: - -1. **MetadataAgent** - For fetching artist and album information, images, etc. -2. **Scrobbler** - For implementing scrobbling functionality with external services -3. **SchedulerCallback** - For executing code after a specified delay or on a recurring schedule -4. **WebSocketCallback** - For interacting with WebSocket endpoints and handling WebSocket events -5. **LifecycleManagement** - For plugin initialization and configuration (one-time `OnInit` only; not invoked per-request) - -## Plugin Architecture - -The plugin system is built on the following key components: - -### 1. Plugin Manager - -The `Manager` (implemented in `plugins/manager.go`) is the core component that: - -- Scans for plugins in the configured plugins directory -- Loads and compiles plugins -- Provides access to loaded plugins through capability-specific interfaces - -### 2. Plugin Protocol - -Plugins communicate with Navidrome using Protocol Buffers (protobuf) over a WASM runtime. The protocol is defined in `plugins/api/api.proto` which specifies the capabilities and messages that plugins can implement. - -### 3. Plugin Adapters - -Adapters bridge between the plugin API and Navidrome's internal interfaces: - -- `wasmMediaAgent` adapts `MetadataAgent` to the internal `agents.Interface` -- `wasmScrobblerPlugin` adapts `Scrobbler` to the internal `scrobbler.Scrobbler` -- `wasmSchedulerCallback` adapts `SchedulerCallback` to the internal `SchedulerCallback` - -* **Plugin Instance Pooling**: Instances are managed in an internal pool (default 8 max, 1m TTL). -* **WASM Compilation & Caching**: Modules are pre-compiled concurrently (max 2) and cached in `[CacheFolder]/plugins`, reducing startup time. The compilation timeout can be configured via `DevPluginCompilationTimeout` in development. - -### 4. Host Services - -Navidrome provides host services that plugins can call to access functionality like HTTP requests and scheduling. -These services are defined in `plugins/host/` and implemented in corresponding host files: - -- HTTP service (in `plugins/host_http.go`) for making external requests -- Scheduler service (in `plugins/host_scheduler.go`) for scheduling timed events -- Config service (in `plugins/host_config.go`) for accessing plugin-specific configuration -- WebSocket service (in `plugins/host_websocket.go`) for WebSocket communication -- Cache service (in `plugins/host_cache.go`) for TTL-based plugin caching -- Artwork service (in `plugins/host_artwork.go`) for generating public artwork URLs -- SubsonicAPI service (in `plugins/host_subsonicapi.go`) for accessing Navidrome's Subsonic API - -### Available Host Services - -The following host services are available to plugins: - -#### HttpService - -```protobuf -// HTTP methods available to plugins -service HttpService { - rpc Get(HttpRequest) returns (HttpResponse); - rpc Post(HttpRequest) returns (HttpResponse); - rpc Put(HttpRequest) returns (HttpResponse); - rpc Delete(HttpRequest) returns (HttpResponse); - rpc Patch(HttpRequest) returns (HttpResponse); - rpc Head(HttpRequest) returns (HttpResponse); - rpc Options(HttpRequest) returns (HttpResponse); -} -``` - -#### ConfigService - -```protobuf -service ConfigService { - rpc GetPluginConfig(GetPluginConfigRequest) returns (GetPluginConfigResponse); -} -``` - -The ConfigService allows plugins to access plugin-specific configuration. See the [config.proto](host/config/config.proto) file for the full API. - -#### ArtworkService - -```protobuf -service ArtworkService { - rpc GetArtistUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse); - rpc GetAlbumUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse); - rpc GetTrackUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse); -} -``` - -Provides methods to get public URLs for artwork images: - -- `GetArtistUrl(id string, size int) string`: Returns a public URL for an artist's artwork -- `GetAlbumUrl(id string, size int) string`: Returns a public URL for an album's artwork -- `GetTrackUrl(id string, size int) string`: Returns a public URL for a track's artwork - -The `size` parameter is optional (use 0 for original size). The URLs returned are based on the server's ShareURL configuration. - -Example: - -```go -url := artwork.GetArtistUrl("123", 300) // Get artist artwork URL with size 300px -url := artwork.GetAlbumUrl("456", 0) // Get album artwork URL in original size -``` - -#### CacheService - -```protobuf -service CacheService { - // Set a string value in the cache - rpc SetString(SetStringRequest) returns (SetResponse); - - // Get a string value from the cache - rpc GetString(GetRequest) returns (GetStringResponse); - - // Set an integer value in the cache - rpc SetInt(SetIntRequest) returns (SetResponse); - - // Get an integer value from the cache - rpc GetInt(GetRequest) returns (GetIntResponse); - - // Set a float value in the cache - rpc SetFloat(SetFloatRequest) returns (SetResponse); - - // Get a float value from the cache - rpc GetFloat(GetRequest) returns (GetFloatResponse); - - // Set a byte slice value in the cache - rpc SetBytes(SetBytesRequest) returns (SetResponse); - - // Get a byte slice value from the cache - rpc GetBytes(GetRequest) returns (GetBytesResponse); - - // Remove a value from the cache - rpc Remove(RemoveRequest) returns (RemoveResponse); - - // Check if a key exists in the cache - rpc Has(HasRequest) returns (HasResponse); -} -``` - -The CacheService provides a TTL-based cache for plugins. Each plugin gets its own isolated cache instance. By default, cached items expire after 24 hours unless a custom TTL is specified. - -Key features: - -- **Isolated Caches**: Each plugin has its own cache namespace, so different plugins can use the same key names without conflicts -- **Typed Values**: Store and retrieve values with their proper types (string, int64, float64, or byte slice) -- **Configurable TTL**: Set custom expiration times per item, or use the default 24-hour TTL -- **Type Safety**: The system handles type checking, returning "not exists" if there's a type mismatch - -Example usage: - -```go -// Store a string value with default TTL (24 hours) -cacheService.SetString(ctx, &cache.SetStringRequest{ - Key: "user_preference", - Value: "dark_mode", -}) - -// Store an integer with custom TTL (5 minutes) -cacheService.SetInt(ctx, &cache.SetIntRequest{ - Key: "api_call_count", - Value: 42, - TtlSeconds: 300, // 5 minutes -}) - -// Retrieve a value -resp, err := cacheService.GetString(ctx, &cache.GetRequest{ - Key: "user_preference", -}) -if err != nil { - // Handle error -} -if resp.Exists { - // Use resp.Value -} else { - // Key doesn't exist or has expired -} - -// Check if a key exists -hasResp, err := cacheService.Has(ctx, &cache.HasRequest{ - Key: "api_call_count", -}) -if hasResp.Exists { - // Key exists and hasn't expired -} - -// Remove a value -cacheService.Remove(ctx, &cache.RemoveRequest{ - Key: "user_preference", -}) -``` - -See the [cache.proto](host/cache/cache.proto) file for the full API definition. - -#### SchedulerService - -The SchedulerService provides a unified interface for scheduling both one-time and recurring tasks, as well as accessing current time information. See the [scheduler.proto](host/scheduler/scheduler.proto) file for the full API. - -```protobuf -service SchedulerService { - // One-time event scheduling - rpc ScheduleOneTime(ScheduleOneTimeRequest) returns (ScheduleResponse); - - // Recurring event scheduling - rpc ScheduleRecurring(ScheduleRecurringRequest) returns (ScheduleResponse); - - // Cancel any scheduled job - rpc CancelSchedule(CancelRequest) returns (CancelResponse); - - // Get current time in multiple formats - rpc TimeNow(TimeNowRequest) returns (TimeNowResponse); -} -``` - -**Key Features:** - -- **One-time scheduling**: Schedule a callback to be executed once after a specified delay. -- **Recurring scheduling**: Schedule a callback to be executed repeatedly according to a cron expression. -- **Current time access**: Get the current time in standardized formats for time-based operations. - -**TimeNow Function:** - -The `TimeNow` function returns the current time in three formats: - -```protobuf -message TimeNowResponse { - string rfc3339_nano = 1; // RFC3339 format with nanosecond precision - int64 unix_milli = 2; // Unix timestamp in milliseconds - string local_time_zone = 3; // Local timezone name (e.g., "UTC", "America/New_York") -} -``` - -This allows plugins to: - -- Get high-precision timestamps for logging and event correlation -- Perform time-based calculations using Unix timestamps -- Handle timezone-aware operations by knowing the server's local timezone - -Example usage: - -```go -// Get current time information -timeResp, err := scheduler.TimeNow(ctx, &scheduler.TimeNowRequest{}) -if err != nil { - return err -} - -// Use the different time formats -timestamp := timeResp.Rfc3339Nano // "2024-01-15T10:30:45.123456789Z" -unixMs := timeResp.UnixMilli // 1705312245123 -timezone := timeResp.LocalTimeZone // "UTC" -``` - -Plugins using this service must implement the `SchedulerCallback` interface: - -```protobuf -service SchedulerCallback { - rpc OnSchedulerCallback(SchedulerCallbackRequest) returns (SchedulerCallbackResponse); -} -``` - -The `IsRecurring` field in the request allows plugins to differentiate between one-time and recurring callbacks. - -#### WebSocketService - -The WebSocketService enables plugins to connect to and interact with WebSocket endpoints. See the [websocket.proto](host/websocket/websocket.proto) file for the full API. - -```protobuf -service WebSocketService { - // Connect to a WebSocket endpoint - rpc Connect(ConnectRequest) returns (ConnectResponse); - - // Send a text message - rpc SendText(SendTextRequest) returns (SendTextResponse); - - // Send binary data - rpc SendBinary(SendBinaryRequest) returns (SendBinaryResponse); - - // Close a connection - rpc Close(CloseRequest) returns (CloseResponse); -} -``` - -- **Connect**: Establish a WebSocket connection to a specified URL with optional headers -- **SendText**: Send text messages over an established connection -- **SendBinary**: Send binary data over an established connection -- **Close**: Close a WebSocket connection with optional close code and reason - -Plugins using this service must implement the `WebSocketCallback` interface to handle incoming messages and connection events: - -```protobuf -service WebSocketCallback { - rpc OnTextMessage(OnTextMessageRequest) returns (OnTextMessageResponse); - rpc OnBinaryMessage(OnBinaryMessageRequest) returns (OnBinaryMessageResponse); - rpc OnError(OnErrorRequest) returns (OnErrorResponse); - rpc OnClose(OnCloseRequest) returns (OnCloseResponse); -} -``` - -Example usage: - -```go -// Connect to a WebSocket server -connectResp, err := websocket.Connect(ctx, &websocket.ConnectRequest{ - Url: "wss://example.com/ws", - Headers: map[string]string{"Authorization": "Bearer token"}, - ConnectionId: "my-connection-id", -}) -if err != nil { - return err -} - -// Send a text message -_, err = websocket.SendText(ctx, &websocket.SendTextRequest{ - ConnectionId: "my-connection-id", - Message: "Hello WebSocket", -}) - -// Send binary data -_, err = websocket.SendBinary(ctx, &websocket.SendBinaryRequest{ - ConnectionId: "my-connection-id", - Data: []byte{0x01, 0x02, 0x03}, -}) - -// Close the connection when done -_, err = websocket.Close(ctx, &websocket.CloseRequest{ - ConnectionId: "my-connection-id", - Code: 1000, // Normal closure - Reason: "Done", -}) -``` - -#### SubsonicAPIService - -```protobuf -service SubsonicAPIService { - rpc Call(CallRequest) returns (CallResponse); -} -``` - -The SubsonicAPIService provides plugins with access to Navidrome's Subsonic API endpoints. This allows plugins to query and interact with Navidrome's music library data using the same API that external Subsonic clients use. - -Key features: - -- **Library Access**: Query artists, albums, tracks, playlists, and other music library data -- **Search Functionality**: Search across the music library using various criteria -- **Metadata Retrieval**: Get detailed information about music items including ratings, play counts, etc. -- **Authentication Handled**: The service automatically handles authentication using internal auth context -- **JSON Responses**: All responses are returned as JSON strings for easy parsing - -**Important Security Notes:** - -- Plugins must specify a username via the `u` parameter in the URL - this determines which user's library view and permissions apply -- The service uses internal authentication, so plugins don't need to provide passwords or API keys -- All Subsonic API security and access controls apply based on the specified user - -Example usage: - -```go -// Get ping response to test connectivity -resp, err := subsonicAPI.Call(ctx, &subsonicapi.CallRequest{ - Url: "/rest/ping?u=admin", -}) -if err != nil { - return err -} -// resp.Json contains the JSON response - -// Search for artists -resp, err = subsonicAPI.Call(ctx, &subsonicapi.CallRequest{ - Url: "/rest/search3?u=admin&query=Beatles&artistCount=10", -}) - -// Get album details -resp, err = subsonicAPI.Call(ctx, &subsonicapi.CallRequest{ - Url: "/rest/getAlbum?u=admin&id=123", -}) - -// Check for errors -if resp.Error != "" { - // Handle error - could be missing parameters, invalid user, etc. - log.Printf("SubsonicAPI error: %s", resp.Error) -} -``` - -**Common URL Patterns:** - -- `/rest/ping?u=USERNAME` - Test API connectivity -- `/rest/search3?u=USERNAME&query=TERM` - Search library -- `/rest/getArtists?u=USERNAME` - Get all artists -- `/rest/getAlbum?u=USERNAME&id=ID` - Get album details -- `/rest/getPlaylists?u=USERNAME` - Get user playlists - -**Required Parameters:** - -- `u` (username): Required for all requests - determines user context and permissions -- `f=json`: Recommended to get JSON responses (easier to parse than XML) - -The service accepts standard Subsonic API endpoints and parameters. Refer to the [Subsonic API documentation](http://www.subsonic.org/pages/api.jsp) for complete endpoint details, but note that authentication parameters (`p`, `t`, `s`, `c`, `v`) are handled automatically. - -See the [subsonicapi.proto](host/subsonicapi/subsonicapi.proto) file for the full API definition. - -## Plugin Permission System - -Navidrome implements a permission-based security system that controls which host services plugins can access. This system enforces security at load-time by only making authorized services available to plugins in their WebAssembly runtime environment. - -### How Permissions Work - -The permission system follows a **secure-by-default** approach: - -1. **Default Behavior**: Plugins have access to **no host services** unless explicitly declared -2. **Load-time Enforcement**: Only services listed in a plugin's permissions are loaded into its WASM runtime -3. **Runtime Security**: Unauthorized services are completely unavailable - attempts to call them result in "function not exported" errors - -This design ensures that even if malicious code tries to access unauthorized services, the calls will fail because the functions simply don't exist in the plugin's runtime environment. - -### Permission Syntax - -Permissions are declared in the plugin's `manifest.json` file using the `permissions` field as an object: - -```json -{ - "name": "my-plugin", - "author": "Plugin Developer", - "version": "1.0.0", - "description": "A plugin that fetches data and caches results", - "website": "https://github.com/plugindeveloper/my-plugin", - "capabilities": ["MetadataAgent"], - "permissions": { - "http": { - "reason": "To fetch metadata from external APIs", - "allowedUrls": { - "https://api.musicbrainz.org": ["GET"], - "https://coverartarchive.org": ["GET"] - }, - "allowLocalNetwork": false - }, - "cache": { - "reason": "To cache API responses and reduce rate limiting" - }, - "subsonicapi": { - "reason": "To query music library for artist and album information", - "allowedUsernames": ["metadata-user"], - "allowAdmins": false - } - } -} -``` - -Each permission is represented as a key in the permissions object. The value must be an object containing a `reason` field that explains why the permission is needed. - -**Important**: Some permissions require additional configuration fields: - -- **`http`**: Requires `allowedUrls` object mapping URL patterns to allowed HTTP methods, and optional `allowLocalNetwork` boolean -- **`websocket`**: Requires `allowedUrls` array of WebSocket URL patterns, and optional `allowLocalNetwork` boolean -- **`subsonicapi`**: Requires `reason` field, with optional `allowedUsernames` array and `allowAdmins` boolean for fine-grained access control -- **`config`**, **`cache`**, **`scheduler`**, **`artwork`**: Only require the `reason` field - -**Security Benefits of Required Reasons:** - -- **Transparency**: Users can see exactly what each plugin will do with its permissions -- **Security Auditing**: Makes it easier to identify suspicious or overly broad permission requests -- **Developer Accountability**: Forces plugin authors to justify each permission they request -- **Trust Building**: Clear explanations help users make informed decisions about plugin installation - -If no permissions are needed, use an empty permissions object: `"permissions": {}`. - -### Available Permissions - -The following permission keys correspond to host services: - -| Permission | Host Service | Description | Required Fields | -| ------------- | ------------------ | -------------------------------------------------- | ----------------------------------------------------- | -| `http` | HttpService | Make HTTP requests (GET, POST, PUT, DELETE, etc..) | `reason`, `allowedUrls` | -| `websocket` | WebSocketService | Connect to and communicate via WebSockets | `reason`, `allowedUrls` | -| `cache` | CacheService | Store and retrieve cached data with TTL | `reason` | -| `config` | ConfigService | Access Navidrome configuration values | `reason` | -| `scheduler` | SchedulerService | Schedule one-time and recurring tasks | `reason` | -| `artwork` | ArtworkService | Generate public URLs for artwork images | `reason` | -| `subsonicapi` | SubsonicAPIService | Access Navidrome's Subsonic API endpoints | `reason`, optional: `allowedUsernames`, `allowAdmins` | - -#### HTTP Permission Structure - -HTTP permissions require explicit URL whitelisting for security: - -```json -{ - "http": { - "reason": "To fetch artist data from MusicBrainz and album covers from Cover Art Archive", - "allowedUrls": { - "https://musicbrainz.org/ws/2/*": ["GET"], - "https://coverartarchive.org/*": ["GET"], - "https://api.example.com/submit": ["POST"] - }, - "allowLocalNetwork": false - } -} -``` - -**Fields:** - -- `reason` (required): Explanation of why HTTP access is needed -- `allowedUrls` (required): Object mapping URL patterns to allowed HTTP methods -- `allowLocalNetwork` (optional, default false): Whether to allow requests to localhost/private IPs - -**URL Pattern Matching:** - -- Exact URLs: `"https://api.example.com/endpoint": ["GET"]` -- Wildcard paths: `"https://api.example.com/*": ["GET", "POST"]` -- Subdomain wildcards: `"https://*.example.com": ["GET"]` - -**Important**: Redirect destinations must also be included in `allowedUrls` if you want to follow redirects. - -#### WebSocket Permission Structure - -WebSocket permissions require explicit URL whitelisting: - -```json -{ - "websocket": { - "reason": "To connect to Discord gateway for real-time Rich Presence updates", - "allowedUrls": ["wss://gateway.discord.gg", "wss://*.discord.gg"], - "allowLocalNetwork": false - } -} -``` - -**Fields:** - -- `reason` (required): Explanation of why WebSocket access is needed -- `allowedUrls` (required): Array of WebSocket URL patterns (must start with `ws://` or `wss://`) -- `allowLocalNetwork` (optional, default false): Whether to allow connections to localhost/private IPs - -#### SubsonicAPI Permission Structure - -SubsonicAPI permissions control which users plugins can access Navidrome's Subsonic API as, providing fine-grained security controls: - -```json -{ - "subsonicapi": { - "reason": "To query music library data for recommendation engine", - "allowedUsernames": ["plugin-user", "readonly-user"], - "allowAdmins": false - } -} -``` - -**Fields:** - -- `reason` (required): Explanation of why SubsonicAPI access is needed -- `allowedUsernames` (optional): Array of specific usernames the plugin is allowed to use. If empty or omitted, any username can be used -- `allowAdmins` (optional, default false): Whether the plugin can make API calls using admin user accounts - -**Security Model:** - -The SubsonicAPI service enforces strict user-based access controls: - -- **Username Validation**: The plugin must provide a valid `u` (username) parameter in all API calls -- **User Context**: All API responses are filtered based on the specified user's permissions and library access -- **Admin Protection**: By default, plugins cannot use admin accounts for API calls to prevent privilege escalation -- **Username Restrictions**: When `allowedUsernames` is specified, only those users can be used - -**Common Permission Patterns:** - -```jsonc -// Allow any non-admin user (most permissive) -{ - "subsonicapi": { - "reason": "To search music library for metadata enhancement", - "allowAdmins": false - } -} - -// Allow only specific users (most secure) -{ - "subsonicapi": { - "reason": "To access playlists for synchronization with external service", - "allowedUsernames": ["sync-user"], - "allowAdmins": false - } -} - -// Allow admin users (use with caution) -{ - "subsonicapi": { - "reason": "To perform administrative tasks like library statistics", - "allowAdmins": true - } -} - -// Restrict to specific users but allow admins -{ - "subsonicapi": { - "reason": "To backup playlists for authorized users only", - "allowedUsernames": ["backup-admin", "user1", "user2"], - "allowAdmins": true - } -} -``` - -**Important Notes:** - -- Username matching is case-insensitive -- If `allowedUsernames` is empty or omitted, any username can be used (subject to `allowAdmins` setting) -- Admin restriction (`allowAdmins: false`) is checked after username validation -- Invalid or non-existent usernames will result in API call errors - -### Permission Validation - -The plugin system validates permissions during loading: - -1. **Schema Validation**: The manifest is validated against the JSON schema -2. **Permission Recognition**: Unknown permission keys are silently accepted for forward compatibility -3. **Service Loading**: Only services with corresponding permissions are made available to the plugin - -### Security Model - -The permission system provides multiple layers of security: - -#### 1. Principle of Least Privilege - -- Plugins start with zero permissions -- Only explicitly requested services are available -- No way to escalate privileges at runtime - -#### 2. Load-time Enforcement - -- Unauthorized services are not loaded into the WASM runtime -- No performance overhead for permission checks during execution -- Impossible to bypass restrictions through code manipulation - -#### 3. Service Isolation - -- Each plugin gets its own isolated service instances -- Plugins cannot interfere with each other's service usage -- Host services are sandboxed within the WASM environment - -### Best Practices for Plugin Developers - -#### Request Minimal Permissions - -```jsonc -// Good: No permissions if none needed -{ - "permissions": {} -} - -// Good: Only request what you need with clear reasoning -{ - "permissions": { - "http": { - "reason": "To fetch artist biography from MusicBrainz database", - "allowedUrls": { - "https://musicbrainz.org/ws/2/artist/*": ["GET"] - }, - "allowLocalNetwork": false - } - } -} - -// Avoid: Requesting unnecessary permissions -{ - "permissions": { - "http": { - "reason": "To fetch data", - "allowedUrls": { - "https://*": ["*"] - }, - "allowLocalNetwork": true - }, - "cache": { - "reason": "For caching" - }, - "scheduler": { - "reason": "For scheduling" - }, - "websocket": { - "reason": "For real-time updates", - "allowedUrls": ["wss://*"], - "allowLocalNetwork": true - } - } -} -``` - -#### Write Clear Permission Reasons - -Provide specific, descriptive reasons for each permission that explain exactly what the plugin does. Good reasons should: - -- Specify **what data** will be accessed/fetched -- Mention **which external services** will be contacted (if applicable) -- Explain **why** the permission is necessary for the plugin's functionality -- Use clear, non-technical language that users can understand - -```jsonc -// Good: Specific and informative -{ - "http": { - "reason": "To fetch album reviews from AllMusic API and artist biographies from MusicBrainz", - "allowedUrls": { - "https://www.allmusic.com/api/*": ["GET"], - "https://musicbrainz.org/ws/2/*": ["GET"] - }, - "allowLocalNetwork": false - }, - "cache": { - "reason": "To cache API responses for 24 hours to respect rate limits and improve performance" - } -} - -// Bad: Vague and unhelpful -{ - "http": { - "reason": "To make requests", - "allowedUrls": { - "https://*": ["*"] - }, - "allowLocalNetwork": true - }, - "cache": { - "reason": "For caching" - } -} -``` - -#### Handle Missing Permissions Gracefully - -Your plugin should provide clear error messages when permissions are missing: - -```go -func (p *Plugin) GetArtistInfo(ctx context.Context, req *api.ArtistInfoRequest) (*api.ArtistInfoResponse, error) { - // This will fail with "function not exported" if http permission is missing - resp, err := p.httpClient.Get(ctx, &http.HttpRequest{Url: apiURL}) - if err != nil { - // Check if it's a permission error - if strings.Contains(err.Error(), "not exported") { - return &api.ArtistInfoResponse{ - Error: "Plugin requires 'http' permission (reason: 'To fetch artist metadata from external APIs') - please add to manifest.json", - }, nil - } - return &api.ArtistInfoResponse{Error: err.Error()}, nil - } - // ... process response -} -``` - -### Troubleshooting Permissions - -#### Common Error Messages - -**"function not exported in module env"** - -- Cause: Plugin trying to call a service without proper permission -- Solution: Add the required permission to your manifest.json - -**"manifest validation failed" or "missing required field"** - -- Cause: Plugin manifest is missing required fields (e.g., `allowedUrls` for HTTP/WebSocket permissions) -- Solution: Ensure your manifest includes all required fields for each permission type - -**Permission silently ignored** - -- Cause: Using a permission key not recognized by current Navidrome version -- Effect: The unknown permission is silently ignored (no error or warning) -- Solution: This is actually normal behavior for forward compatibility - -#### Debugging Permission Issues - -1. **Check the manifest**: Ensure required permissions are spelled correctly and present -2. **Verify required fields**: Check that HTTP and WebSocket permissions include `allowedUrls` and other required fields -3. **Review logs**: Check for plugin loading errors, manifest validation errors, and WASM runtime errors -4. **Test incrementally**: Add permissions one at a time to identify which services your plugin needs -5. **Verify service names**: Ensure permission keys match exactly: `http`, `cache`, `config`, `scheduler`, `websocket`, `artwork`, `subsonicapi` -6. **Validate manifest**: Use a JSON schema validator to check your manifest against the schema - -### Future Considerations - -The permission system is designed for extensibility: - -- **Unknown permissions** are allowed in manifests for forward compatibility -- **New services** can be added with corresponding permission keys -- **Permission scoping** could be added in the future (e.g., read-only vs. read-write access) - -This ensures that plugins developed today will continue to work as the system evolves, while maintaining strong security boundaries. - -## Plugin System Implementation - -Navidrome's plugin system is built using the following key libraries: - -### 1. WebAssembly Runtime (Wazero) - -The plugin system uses [Wazero](https://github.com/tetratelabs/wazero), a WebAssembly runtime written in pure Go. Wazero was chosen for several reasons: - -- **No CGO dependency**: Unlike other WebAssembly runtimes, Wazero is implemented in pure Go, which simplifies cross-compilation and deployment. -- **Performance**: It provides efficient compilation and caching of WebAssembly modules. -- **Security**: Wazero enforces strict sandboxing, which is important for running third-party plugin code safely. - -The plugin manager uses Wazero to: - -- Compile and cache WebAssembly modules -- Create isolated runtime environments for each plugin -- Instantiate plugin modules when they're called -- Provide host functions that plugins can call - -### 2. Go-plugin Framework - -Navidrome builds on [go-plugin](https://github.com/knqyf263/go-plugin), a Go plugin system over WebAssembly that provides: - -- **Code generation**: Custom Protocol Buffer compiler plugin (`protoc-gen-go-plugin`) that generates Go code for both the host and WebAssembly plugins -- **Host function system**: Framework for exposing host functionality to plugins safely -- **Interface versioning**: Built-in mechanism for handling API compatibility between the host and plugins -- **Type conversion**: Utilities for marshaling and unmarshaling data between Go and WebAssembly - -This framework significantly simplifies plugin development by handling the low-level details of WebAssembly communication, allowing plugin developers to focus on implementing capabilities interfaces. - -### 3. Protocol Buffers (Protobuf) - -[Protocol Buffers](https://developers.google.com/protocol-buffers) serve as the interface definition language for the plugin system. Navidrome uses: - -- **protoc-gen-go-plugin**: A custom protobuf compiler plugin that generates Go code for both the Navidrome host and WebAssembly plugins -- Protobuf messages for structured data exchange between the host and plugins - -The protobuf definitions are located in: - -- `plugins/api/api.proto`: Core plugin capability interfaces -- `plugins/host/http/http.proto`: HTTP service interface -- `plugins/host/scheduler/scheduler.proto`: Scheduler service interface -- `plugins/host/config/config.proto`: Config service interface -- `plugins/host/websocket/websocket.proto`: WebSocket service interface -- `plugins/host/cache/cache.proto`: Cache service interface -- `plugins/host/artwork/artwork.proto`: Artwork service interface -- `plugins/host/subsonicapi/subsonicapi.proto`: SubsonicAPI service interface - -### 4. Integration Architecture - -The plugin system integrates these libraries through several key components: - -- **Plugin Manager**: Manages the lifecycle of plugins, from discovery to loading -- **Compilation Cache**: Improves performance by caching compiled WebAssembly modules -- **Host Function Bridge**: Exposes Navidrome functionality to plugins through WebAssembly imports -- **Capability Adapters**: Convert between the plugin API and Navidrome's internal interfaces - -Each plugin method call: - -1. Creates a new isolated plugin instance using Wazero -2. Executes the method in the sandboxed environment -3. Converts data between Go and WebAssembly formats using the protobuf-generated code -4. Cleans up the instance after the call completes - -This stateless design ensures that plugins remain isolated and can't interfere with Navidrome's core functionality or each other. - -## Configuration - -Plugins are configured in Navidrome's main configuration via the `Plugins` section: - -```toml -[Plugins] -# Enable or disable plugin support -Enabled = true - -# Directory where plugins are stored (defaults to [DataFolder]/plugins) -Folder = "/path/to/plugins" -``` - -By default, the plugins folder is created under `[DataFolder]/plugins` with restrictive permissions (`0700`) to limit access to the Navidrome user. - -### Plugin-specific Configuration - -You can also provide plugin-specific configuration using the `PluginConfig` section. Each plugin can have its own configuration map using the **folder name** as the key: - -```toml -[PluginConfig.my-plugin-folder] -api_key = "your-api-key" -user_id = "your-user-id" -enable_feature = "true" - -[PluginConfig.another-plugin-folder] -server_url = "https://example.com/api" -timeout = "30" -``` - -These configuration values are passed to plugins during initialization through the `OnInit` method in the `LifecycleManagement` capability. -Plugins that implement the `LifecycleManagement` capability will receive their configuration as a map of string keys and values. - -## Plugin Directory Structure - -Each plugin must be located in its own directory under the plugins folder: - -``` -plugins/ -├── my-plugin/ -│ ├── plugin.wasm # Compiled WebAssembly module -│ └── manifest.json # Plugin manifest defining metadata and capabilities -├── another-plugin/ -│ ├── plugin.wasm -│ └── manifest.json -``` - -**Note**: Plugin identification has changed! Navidrome now uses the **folder name** as the unique identifier for plugins, not the `name` field in `manifest.json`. This means: - -- **Multiple plugins can have the same `name` in their manifest**, as long as they are in different folders -- **Plugin loading and commands use the folder name**, not the manifest name -- **Folder names must be unique** across all plugins in your plugins directory - -This change allows you to have multiple versions or variants of the same plugin (e.g., `lastfm-official`, `lastfm-custom`, `lastfm-dev`) that all have the same manifest name but coexist peacefully. - -### Example: Multiple Plugin Variants - -``` -plugins/ -├── lastfm-official/ -│ ├── plugin.wasm -│ └── manifest.json # {"name": "LastFM Agent", ...} -├── lastfm-custom/ -│ ├── plugin.wasm -│ └── manifest.json # {"name": "LastFM Agent", ...} -└── lastfm-dev/ - ├── plugin.wasm - └── manifest.json # {"name": "LastFM Agent", ...} -``` - -All three plugins can have the same `"name": "LastFM Agent"` in their manifest, but they are identified and loaded by their folder names: - -```bash -# Load specific variants -navidrome plugin refresh lastfm-official -navidrome plugin refresh lastfm-custom -navidrome plugin refresh lastfm-dev - -# Configure each variant separately -[PluginConfig.lastfm-official] -api_key = "production-key" - -[PluginConfig.lastfm-dev] -api_key = "development-key" -``` - -### Using Symlinks for Plugin Variants - -Symlinks provide a powerful way to create multiple configurations for the same plugin without duplicating files. When you create a symlink to a plugin directory, Navidrome treats the symlink as a separate plugin with its own configuration. - -**Example: Discord Rich Presence with Multiple Configurations** - -```bash -# Create symlinks for different environments -cd /path/to/navidrome/plugins -ln -s /path/to/discord-rich-presence-plugin drp-prod -ln -s /path/to/discord-rich-presence-plugin drp-dev -ln -s /path/to/discord-rich-presence-plugin drp-test -``` - -Directory structure: - -``` -plugins/ -├── drp-prod -> /path/to/discord-rich-presence-plugin/ -├── drp-dev -> /path/to/discord-rich-presence-plugin/ -├── drp-test -> /path/to/discord-rich-presence-plugin/ -``` - -Each symlink can have its own configuration: - -```toml -[PluginConfig.drp-prod] -clientid = "production-client-id" -users = "admin:prod-token" - -[PluginConfig.drp-dev] -clientid = "development-client-id" -users = "admin:dev-token,testuser:test-token" - -[PluginConfig.drp-test] -clientid = "test-client-id" -users = "testuser:test-token" -``` - -**Key Benefits:** - -- **Single Source**: One plugin implementation serves multiple use cases -- **Independent Configuration**: Each symlink has its own configuration namespace -- **Development Workflow**: Easy to test different configurations without code changes -- **Resource Sharing**: All symlinks share the same compiled WASM binary - -**Important Notes:** - -- The **symlink name** (not the target folder name) is used as the plugin ID -- Configuration keys use the symlink name: `PluginConfig.` -- Each symlink appears as a separate plugin in `navidrome plugin list` -- CLI commands use the symlink name: `navidrome plugin refresh drp-dev` - -## Plugin Package Format (.ndp) - -Navidrome Plugin Packages (.ndp) are ZIP archives that bundle all files needed for a plugin. They can be installed using the `navidrome plugin install` command. - -### Package Structure - -A valid .ndp file must contain: - -``` -plugin-name.ndp (ZIP file) -├── plugin.wasm # Required: The compiled WebAssembly module -├── manifest.json # Required: Plugin manifest with metadata -├── README.md # Optional: Documentation -└── LICENSE # Optional: License information -``` - -### Creating a Plugin Package - -To create a plugin package: - -1. Compile your plugin to WebAssembly (plugin.wasm) -2. Create a manifest.json file with required fields -3. Include any documentation files you want to bundle -4. Create a ZIP archive of all files -5. Rename the ZIP file to have a .ndp extension - -### Installing a Plugin Package - -Use the Navidrome CLI to install plugins: - -```bash -navidrome plugin install /path/to/plugin-name.ndp -``` - -This will extract the plugin to a directory in your configured plugins folder. - -## Plugin Management - -Navidrome provides a command-line interface for managing plugins. To use these commands, the plugin system must be enabled in your configuration. - -### Available Commands - -```bash -# List all installed plugins -navidrome plugin list - -# Show detailed information about a plugin package or installed plugin -navidrome plugin info plugin-name-or-package.ndp - -# Install a plugin from a .ndp file -navidrome plugin install /path/to/plugin.ndp - -# Remove an installed plugin (use folder name) -navidrome plugin remove plugin-folder-name - -# Update an existing plugin -navidrome plugin update /path/to/updated-plugin.ndp - -# Reload a plugin without restarting Navidrome (use folder name) -navidrome plugin refresh plugin-folder-name - -# Create a symlink to a plugin development folder -navidrome plugin dev /path/to/dev/folder -``` - -### Plugin Development - -The `dev` and `refresh` commands are particularly useful for plugin development: - -#### Development Workflow - -1. Create a plugin development folder with required files (`manifest.json` and `plugin.wasm`) -2. Run `navidrome plugin dev /path/to/your/plugin` to create a symlink in the plugins directory -3. Make changes to your plugin code -4. Recompile the WebAssembly module -5. Run `navidrome plugin refresh your-plugin-folder-name` to reload the plugin without restarting Navidrome - -The `dev` command creates a symlink from your development folder to the plugins directory, allowing you to edit the plugin files directly in your development environment without copying them to the plugins directory after each change. - -The refresh process: - -- Reloads the plugin manifest -- Recompiles the WebAssembly module -- Updates the plugin registration -- Makes the updated plugin immediately available to Navidrome - -### Plugin Security - -Navidrome provides multiple layers of security for plugin execution: - -1. **WebAssembly Sandbox**: Plugins run in isolated WebAssembly environments with no direct system access -2. **Permission System**: Plugins can only access host services they explicitly request in their manifest (see [Plugin Permission System](#plugin-permission-system)) -3. **File System Security**: The plugins folder is configured with restricted permissions (0700) accessible only by the user running Navidrome -4. **Resource Isolation**: Each plugin instance is isolated and cannot interfere with other plugins or core Navidrome functionality - -The permission system ensures that plugins follow the principle of least privilege - they start with no access to host services and must explicitly declare what they need. This prevents malicious or poorly written plugins from accessing unauthorized functionality. - -Always ensure you trust the source of any plugins you install, and review their requested permissions before installation. - -## Plugin Manifest - -**Capability Names Are Case-Sensitive**: Entries in the `capabilities` array must exactly match one of the supported capabilities: `MetadataAgent`, `Scrobbler`, `SchedulerCallback`, `WebSocketCallback`, or `LifecycleManagement`. -**Manifest Validation**: The `manifest.json` is validated against the embedded JSON schema (`plugins/schema/manifest.schema.json`). Invalid manifests will be rejected during plugin discovery. - -Every plugin must provide a `manifest.json` file that declares metadata, capabilities, and permissions: - -```json -{ - "name": "my-awesome-plugin", - "author": "Your Name", - "version": "1.0.0", - "description": "A plugin that does awesome things", - "website": "https://github.com/yourname/my-awesome-plugin", - "capabilities": [ - "MetadataAgent", - "Scrobbler", - "SchedulerCallback", - "WebSocketCallback", - "LifecycleManagement" - ], - "permissions": { - "http": { - "reason": "To fetch metadata from external music APIs" - }, - "cache": { - "reason": "To cache API responses and reduce rate limiting" - }, - "config": { - "reason": "To read API keys and service configuration" - }, - "scheduler": { - "reason": "To schedule periodic data refresh tasks" - } - } -} -``` - -Required fields: - -- `name`: Display name of the plugin (used for documentation/display purposes; folder name is used for identification) -- `author`: The creator or organization behind the plugin -- `version`: Version identifier (recommended to follow semantic versioning) -- `description`: A brief description of what the plugin does -- `website`: Website URL for the plugin documentation, source code, or homepage (must be a valid URI) -- `capabilities`: Array of capability types the plugin implements -- `permissions`: Object mapping host service names to their configurations (use empty object `{}` for no permissions) - -Currently supported capabilities: - -- `MetadataAgent` - For implementing media metadata providers -- `Scrobbler` - For implementing scrobbling plugins -- `SchedulerCallback` - For implementing timed callbacks -- `WebSocketCallback` - For interacting with WebSocket endpoints and handling WebSocket events -- `LifecycleManagement` - For handling plugin initialization and configuration - -## Plugin Loading Process - -1. The Plugin Manager scans the plugins directory and all subdirectories -2. For each subdirectory containing a `plugin.wasm` file and valid `manifest.json`, the manager: - - Validates the manifest and checks for supported capabilities - - Pre-compiles the WASM module in the background - - Registers the plugin using the **folder name** as the unique identifier in the plugin registry -3. Plugins can be loaded on-demand by folder name or all at once, depending on the manager's method calls - -## Writing a Plugin - -### Requirements - -1. Your plugin must be compiled to WebAssembly (WASM) -2. Your plugin must implement at least one of the capability interfaces defined in `api.proto` -3. Your plugin must be placed in its own directory with a proper `manifest.json` - -### Plugin Registration Functions - -The plugin API provides several registration functions that plugins can call during initialization to register capabilities and obtain host services. These functions should typically be called in your plugin's `init()` function. - -#### Standard Registration Functions - -```go -func RegisterMetadataAgent(agent MetadataAgent) -func RegisterScrobbler(scrobbler Scrobbler) -func RegisterSchedulerCallback(callback SchedulerCallback) -func RegisterLifecycleManagement(lifecycle LifecycleManagement) -func RegisterWebSocketCallback(callback WebSocketCallback) -``` - -These functions register plugins for the standard capability interfaces: - -- **RegisterMetadataAgent**: Register a plugin that provides artist/album metadata and images -- **RegisterScrobbler**: Register a plugin that handles scrobbling to external services -- **RegisterSchedulerCallback**: Register a plugin that handles scheduled callbacks (single callback per plugin) -- **RegisterLifecycleManagement**: Register a plugin that handles initialization and configuration -- **RegisterWebSocketCallback**: Register a plugin that handles WebSocket events - -**Basic Usage Example:** - -```go -type MyPlugin struct { - // plugin implementation -} - -func init() { - plugin := &MyPlugin{} - - // Register capabilities your plugin implements - api.RegisterScrobbler(plugin) - api.RegisterLifecycleManagement(plugin) -} -``` - -#### RegisterNamedSchedulerCallback - -```go -func RegisterNamedSchedulerCallback(name string, cb SchedulerCallback) scheduler.SchedulerService -``` - -This function registers a named scheduler callback and returns a scheduler service instance. Named callbacks allow a single plugin to register multiple scheduler callbacks for different purposes, each with its own identifier. - -**Parameters:** - -- `name` (string): A unique identifier for this scheduler callback within the plugin. This name is used to route scheduled events to the correct callback handler. -- `cb` (SchedulerCallback): An object that implements the `SchedulerCallback` interface - -**Returns:** - -- `scheduler.SchedulerService`: A scheduler service instance that can be used to schedule one-time or recurring tasks for this specific callback - -**Usage Example** (from Discord Rich Presence plugin): - -```go -func init() { - // Register multiple named scheduler callbacks for different purposes - plugin.sched = api.RegisterNamedSchedulerCallback("close-activity", plugin) - plugin.rpc.sched = api.RegisterNamedSchedulerCallback("heartbeat", plugin.rpc) -} - -// The plugin implements SchedulerCallback to handle "close-activity" events -func (d *DiscordRPPlugin) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) { - log.Printf("Removing presence for user %s", req.ScheduleId) - // Handle close-activity scheduling events - return nil, d.rpc.clearActivity(ctx, req.ScheduleId) -} - -// The rpc component implements SchedulerCallback to handle "heartbeat" events -func (r *discordRPC) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) { - // Handle heartbeat scheduling events - return nil, r.sendHeartbeat(ctx, req.ScheduleId) -} - -// Use the returned scheduler service to schedule tasks -func (d *DiscordRPPlugin) NowPlaying(ctx context.Context, request *api.ScrobblerNowPlayingRequest) (*api.ScrobblerNowPlayingResponse, error) { - // Schedule a one-time callback to clear activity when track ends - _, err = d.sched.ScheduleOneTime(ctx, &scheduler.ScheduleOneTimeRequest{ - ScheduleId: request.Username, - DelaySeconds: request.Track.Length - request.Track.Position + 5, - }) - return nil, err -} - -func (r *discordRPC) connect(ctx context.Context, username string, token string) error { - // Schedule recurring heartbeats for Discord connection - _, err := r.sched.ScheduleRecurring(ctx, &scheduler.ScheduleRecurringRequest{ - CronExpression: "@every 41s", - ScheduleId: username, - }) - return err -} -``` - -**Key Benefits:** - -- **Multiple Schedulers**: A single plugin can have multiple named scheduler callbacks for different purposes (e.g., "heartbeat", "cleanup", "refresh") -- **Isolated Scheduling**: Each named callback gets its own scheduler service, allowing independent scheduling management -- **Clear Separation**: Different callback handlers can be implemented on different objects within your plugin -- **Flexible Routing**: The scheduler automatically routes callbacks to the correct handler based on the registration name - -**Important Notes:** - -- The `name` parameter must be unique within your plugin, but can be the same across different plugins -- The returned scheduler service is specifically tied to the named callback you registered -- Scheduled events will call the `OnSchedulerCallback` method on the object you provided during registration -- You must implement the `SchedulerCallback` interface on the object you register - -#### RegisterSchedulerCallback vs RegisterNamedSchedulerCallback - -- **Use `RegisterSchedulerCallback`** when your plugin only needs a single scheduler callback -- **Use `RegisterNamedSchedulerCallback`** when your plugin needs multiple scheduler callbacks for different purposes (like the Discord plugin's "heartbeat" and "close-activity" callbacks) - -The named version allows better organization and separation of concerns when you have complex scheduling requirements. - -### Capability Interfaces - -#### Metadata Agent - -A capability fetches metadata about artists and albums. Implement this interface to add support for fetching data from external sources. - -```protobuf -service MetadataAgent { - // Artist metadata methods - rpc GetArtistMBID(ArtistMBIDRequest) returns (ArtistMBIDResponse); - rpc GetArtistURL(ArtistURLRequest) returns (ArtistURLResponse); - rpc GetArtistBiography(ArtistBiographyRequest) returns (ArtistBiographyResponse); - rpc GetSimilarArtists(ArtistSimilarRequest) returns (ArtistSimilarResponse); - rpc GetArtistImages(ArtistImageRequest) returns (ArtistImageResponse); - rpc GetArtistTopSongs(ArtistTopSongsRequest) returns (ArtistTopSongsResponse); - - // Album metadata methods - rpc GetAlbumInfo(AlbumInfoRequest) returns (AlbumInfoResponse); - rpc GetAlbumImages(AlbumImagesRequest) returns (AlbumImagesResponse); -} -``` - -#### Scrobbler - -This capability enables scrobbling to external services. Implement this interface to add support for custom scrobblers. - -```protobuf -service Scrobbler { - rpc IsAuthorized(ScrobblerIsAuthorizedRequest) returns (ScrobblerIsAuthorizedResponse); - rpc NowPlaying(ScrobblerNowPlayingRequest) returns (ScrobblerNowPlayingResponse); - rpc Scrobble(ScrobblerScrobbleRequest) returns (ScrobblerScrobbleResponse); -} -``` - -#### Scheduler Callback - -This capability allows plugins to receive one-time or recurring scheduled callbacks. Implement this interface to add -support for scheduled tasks. See the [SchedulerService](#scheduler-service) for more information. - -```protobuf -service SchedulerCallback { - rpc OnSchedulerCallback(SchedulerCallbackRequest) returns (SchedulerCallbackResponse); -} -``` - -#### WebSocket Callback - -This capability allows plugins to interact with WebSocket endpoints and handle WebSocket events. Implement this interface to add support for WebSocket-based communication. - -```protobuf -service WebSocketCallback { - // Called when a text message is received - rpc OnTextMessage(OnTextMessageRequest) returns (OnTextMessageResponse); - - // Called when a binary message is received - rpc OnBinaryMessage(OnBinaryMessageRequest) returns (OnBinaryMessageResponse); - - // Called when an error occurs - rpc OnError(OnErrorRequest) returns (OnErrorResponse); - - // Called when the connection is closed - rpc OnClose(OnCloseRequest) returns (OnCloseResponse); -} -``` - -Plugins can use the WebSocket host service to connect to WebSocket endpoints, send messages, and handle responses: - -```go -// Define a connection ID first -connectionID := "my-connection-id" - -// Connect to a WebSocket server -connectResp, err := websocket.Connect(ctx, &websocket.ConnectRequest{ - Url: "wss://example.com/ws", - Headers: map[string]string{"Authorization": "Bearer token"}, - ConnectionId: connectionID, -}) -if err != nil { - return err -} - -// Send a text message -_, err = websocket.SendText(ctx, &websocket.SendTextRequest{ - ConnectionId: connectionID, - Message: "Hello WebSocket", -}) - -// Close the connection when done -_, err = websocket.Close(ctx, &websocket.CloseRequest{ - ConnectionId: connectionID, - Code: 1000, // Normal closure - Reason: "Done", -}) -``` - -## Host Services - -Navidrome provides several host services that plugins can use to interact with external systems and access functionality. Plugins must declare permissions for each service they want to use in their `manifest.json`. - -### HTTP Service - -The HTTP service allows plugins to make HTTP requests to external APIs and services. To use this service, declare the `http` permission in your manifest. - -#### Basic Usage - -```json -{ - "permissions": { - "http": { - "reason": "To fetch artist metadata from external music APIs" - } - } -} -``` - -#### Granular Permissions - -For enhanced security, you can specify granular HTTP permissions that restrict which URLs and HTTP methods your plugin can access: - -```json -{ - "permissions": { - "http": { - "reason": "To fetch album reviews from AllMusic and artist data from MusicBrainz", - "allowedUrls": { - "https://api.allmusic.com": ["GET", "POST"], - "https://*.musicbrainz.org": ["GET"], - "https://coverartarchive.org": ["GET"], - "*": ["GET"] - }, - "allowLocalNetwork": false - } - } -} -``` - -**Permission Fields:** - -- `reason` (required): Clear explanation of why HTTP access is needed -- `allowedUrls` (required): Map of URL patterns to allowed HTTP methods - - - Must contain at least one URL pattern - - For unrestricted access, use: `{"*": ["*"]}` - - Keys can be exact URLs, wildcard patterns, or `*` for any URL - - Values are arrays of HTTP methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or `*` for any method - - **Important**: Redirect destinations must also be included in this list. If a URL redirects to another URL not in `allowedUrls`, the redirect will be blocked. - -- `allowLocalNetwork` (optional, default: `false`): Whether to allow requests to localhost/private IPs - -**URL Pattern Matching:** - -- Exact URLs: `https://api.example.com` -- Wildcard subdomains: `https://*.example.com` (matches any subdomain) -- Wildcard paths: `https://example.com/api/*` (matches any path under /api/) -- Global wildcard: `*` (matches any URL - use with caution) - -**Examples:** - -```json -// Allow only GET requests to specific APIs -{ - "allowedUrls": { - "https://api.last.fm": ["GET"], - "https://ws.audioscrobbler.com": ["GET"] - } -} - -// Allow any method to a trusted domain, GET everywhere else -{ - "allowedUrls": { - "https://my-trusted-api.com": ["*"], - "*": ["GET"] - } -} - -// Handle redirects by including redirect destinations -{ - "allowedUrls": { - "https://short.ly/api123": ["GET"], // Original URL - "https://api.actual-service.com": ["GET"] // Redirect destination - } -} - -// Strict permissions for a secure plugin (blocks redirects by not including redirect destinations) -{ - "allowedUrls": { - "https://api.musicbrainz.org/ws/2": ["GET"] - }, - "allowLocalNetwork": false -} -``` - -#### Security Considerations - -The HTTP service implements several security features: - -1. **Local Network Protection**: By default, requests to localhost and private IP ranges are blocked -2. **URL Filtering**: Only URLs matching `allowedUrls` patterns are allowed -3. **Method Restrictions**: HTTP methods are validated against the allowed list for each URL pattern -4. **Redirect Security**: - - Redirect destinations must also match `allowedUrls` patterns and methods - - Maximum of 5 redirects per request to prevent redirect loops - - To block all redirects, simply don't include any redirect destinations in `allowedUrls` - -**Private IP Ranges Blocked (when `allowLocalNetwork: false`):** - -- IPv4: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16` -- IPv6: `::1`, `fe80::/10`, `fc00::/7` -- Hostnames: `localhost` - -#### Making HTTP Requests - -```go -import "github.com/navidrome/navidrome/plugins/host/http" - -// GET request -resp, err := httpClient.Get(ctx, &http.HttpRequest{ - Url: "https://api.example.com/data", - Headers: map[string]string{ - "Authorization": "Bearer " + token, - "User-Agent": "MyPlugin/1.0", - }, - TimeoutMs: 5000, -}) - -// POST request with body -resp, err := httpClient.Post(ctx, &http.HttpRequest{ - Url: "https://api.example.com/submit", - Headers: map[string]string{ - "Content-Type": "application/json", - }, - Body: []byte(`{"key": "value"}`), - TimeoutMs: 10000, -}) - -// Handle response -if err != nil { - return &api.Response{Error: "HTTP request failed: " + err.Error()}, nil -} - -if resp.Error != "" { - return &api.Response{Error: "HTTP error: " + resp.Error}, nil -} - -if resp.Status != 200 { - return &api.Response{Error: fmt.Sprintf("HTTP %d: %s", resp.Status, string(resp.Body))}, nil -} - -// Use response data -data := resp.Body -headers := resp.Headers -``` - -### Other Host Services - -#### Config Service - -Access plugin-specific configuration: - -```json -{ - "permissions": { - "config": { - "reason": "To read API keys and service endpoints from plugin configuration" - } - } -} -``` - -#### Cache Service - -Store and retrieve data to improve performance: - -```json -{ - "permissions": { - "cache": { - "reason": "To cache API responses and reduce external service calls" - } - } -} -``` - -#### Scheduler Service - -Schedule recurring or one-time tasks: - -```json -{ - "permissions": { - "scheduler": { - "reason": "To schedule periodic metadata refresh and cleanup tasks" - } - } -} -``` - -#### WebSocket Service - -Connect to WebSocket endpoints: - -```json -{ - "permissions": { - "websocket": { - "reason": "To connect to real-time music service APIs for live data", - "allowedUrls": [ - "wss://api.musicservice.com/ws", - "wss://realtime.example.com" - ], - "allowLocalNetwork": false - } - } -} -``` - -#### Artwork Service - -Generate public URLs for artwork: - -```json -{ - "permissions": { - "artwork": { - "reason": "To generate public URLs for album and artist images" - } - } -} -``` - -### Error Handling - -Plugins should use the standard error values (`plugin:not_found`, `plugin:not_implemented`) to indicate resource-not-found and unimplemented-method scenarios. All other errors will be propagated directly to the caller. Ensure your capability methods return errors via the response message `error` fields rather than panicking or relying on transport errors. - -## Plugin Lifecycle and Statelessness - -**Important**: Navidrome plugins are stateless. Each method call creates a new plugin instance which is destroyed afterward. This has several important implications: - -1. **No in-memory persistence**: Plugins cannot store state between method calls in memory -2. **Each call is isolated**: Variables, configurations, and runtime state don't persist between calls -3. **No shared resources**: Each plugin instance has its own memory space - -This stateless design is crucial for security and stability: - -- Memory leaks in one call won't affect subsequent operations -- A crashed plugin instance won't bring down the entire system -- Resource usage is more predictable and contained - -When developing plugins, keep these guidelines in mind: - -- Don't try to cache data in memory between calls -- Don't store authentication tokens or session data in variables -- If persistence is needed, use external storage or the host's HTTP interface -- Performance optimizations should focus on efficient per-call execution - -### Using Plugin Configuration - -Since plugins are stateless, you can use the `LifecycleManagement` interface to read configuration when your plugin is loaded and perform any necessary setup: - -```go -func (p *myPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { - // Access plugin configuration - apiKey := req.Config["api_key"] - if apiKey == "" { - return &api.InitResponse{Error: "Missing API key in configuration"}, nil - } - - // Validate configuration - serverURL := req.Config["server_url"] - if serverURL == "" { - serverURL = "https://default-api.example.com" // Use default if not specified - } - - // Perform initialization tasks (e.g., validate API key) - httpClient := &http.HttpServiceClient{} - resp, err := httpClient.Get(ctx, &http.HttpRequest{ - Url: serverURL + "/validate?key=" + apiKey, - }) - if err != nil { - return &api.InitResponse{Error: "Failed to validate API key: " + err.Error()}, nil - } - - if resp.StatusCode != 200 { - return &api.InitResponse{Error: "Invalid API key"}, nil - } - - return &api.InitResponse{}, nil -} -``` - -Remember, the `OnInit` method is called only once when the plugin is loaded. It cannot store any state that needs to persist between method calls. It's primarily useful for: - -1. Validating required configuration -2. Checking API credentials -3. Verifying connectivity to external services -4. Initializing any external resources - -## Caching - -The plugin system implements a compilation cache to improve performance: - -1. Compiled WASM modules are cached in `[CacheFolder]/plugins` -2. This reduces startup time for plugins that have already been compiled -3. The cache has a automatic cleanup mechanism to remove old modules. - - when the cache folder exceeds `Plugins.CacheSize` (default 100MB), - the oldest modules are removed - -### WASM Loading Optimization - -To improve performance during plugin instance creation, the system implements an optimization that avoids repeated file reads and compilation: - -1. **Precompilation**: During plugin discovery, WASM files are read and compiled in the background, with both the MD5 hash of the file bytes and compiled modules cached in memory. - -2. **Optimized Runtime**: After precompilation completes, plugins use an `optimizedRuntime` wrapper that overrides `CompileModule` to detect when the same WASM bytes are being compiled by comparing MD5 hashes. - -3. **Cache Hit**: When the generated plugin code calls `os.ReadFile()` and `CompileModule()`, the optimization calculates the MD5 hash of the incoming bytes and compares it with the cached hash. If they match, it returns the pre-compiled module directly. - -4. **Performance Benefit**: This eliminates repeated compilation while using minimal memory (16 bytes per plugin for the MD5 hash vs potentially MB of WASM bytes), significantly improving plugin instance creation speed while maintaining full compatibility with the generated API code. - -5. **Memory Efficiency**: By storing only MD5 hashes instead of full WASM bytes, the optimization scales efficiently regardless of plugin size or count. - -The optimization is transparent to plugin developers and automatically activates when plugins are successfully precompiled. - -## Best Practices - -1. **Resource Management**: - - - The host handles HTTP response cleanup, so no need to close response objects - - Keep plugin instances lightweight as they are created and destroyed frequently - -2. **Error Handling**: - - - Use the standard error types when appropriate - - Return descriptive error messages for debugging - - Custom errors are supported and will be propagated to the caller - -3. **Performance**: - - - Remember plugins are stateless, so don't rely on local variables for caching. Use the CacheService for caching data. - - Use efficient algorithms that work well in single-call scenarios - -4. **Security**: - - Only request permissions you actually need (see [Plugin Permission System](#plugin-permission-system)) - - Validate inputs to prevent injection attacks - - Don't store sensitive credentials in the plugin code - - Use configuration for API keys and sensitive data - -## Limitations - -1. WASM plugins have limited access to system resources -2. Plugin compilation has an initial overhead on first load, as it needs to be compiled to WebAssembly - - Subsequent calls are faster due to caching -3. New plugin capabilities types require changes to the core codebase -4. Stateless nature prevents certain optimizations - -## Troubleshooting - -1. **Plugin not detected**: - - - Ensure `plugin.wasm` and `manifest.json` exist in the plugin directory - - Check that the manifest contains valid capabilities names - - Verify the manifest schema is valid (see [Plugin Permission System](#plugin-permission-system)) - -2. **Permission errors**: - - - **"function not exported in module env"**: Plugin trying to use a service without proper permission - - Check that required permissions are declared in `manifest.json` - - See [Troubleshooting Permissions](#troubleshooting-permissions) for detailed guidance - -3. **Compilation errors**: - - - Check logs for WASM compilation errors - - Verify the plugin is compatible with the current API version - -4. **Runtime errors**: - - Look for error messages in the Navidrome logs - - Add debug logging to your plugin - - Check if the error is permission-related before debugging plugin logic diff --git a/plugins/adapter_media_agent.go b/plugins/adapter_media_agent.go deleted file mode 100644 index eca891275..000000000 --- a/plugins/adapter_media_agent.go +++ /dev/null @@ -1,166 +0,0 @@ -package plugins - -import ( - "context" - - "github.com/navidrome/navidrome/core/agents" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/api" - "github.com/tetratelabs/wazero" -) - -// NewWasmMediaAgent creates a new adapter for a MetadataAgent plugin -func newWasmMediaAgent(wasmPath, pluginID string, m *managerImpl, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin { - loader, err := api.NewMetadataAgentPlugin(context.Background(), api.WazeroRuntime(runtime), api.WazeroModuleConfig(mc)) - if err != nil { - log.Error("Error creating media metadata service plugin", "plugin", pluginID, "path", wasmPath, err) - return nil - } - return &wasmMediaAgent{ - baseCapability: newBaseCapability[api.MetadataAgent, *api.MetadataAgentPlugin]( - wasmPath, - pluginID, - CapabilityMetadataAgent, - m.metrics, - loader, - func(ctx context.Context, l *api.MetadataAgentPlugin, path string) (api.MetadataAgent, error) { - return l.Load(ctx, path) - }, - ), - } -} - -// wasmMediaAgent adapts a MetadataAgent plugin to implement the agents.Interface -type wasmMediaAgent struct { - *baseCapability[api.MetadataAgent, *api.MetadataAgentPlugin] -} - -func (w *wasmMediaAgent) AgentName() string { - return w.id -} - -func (w *wasmMediaAgent) mapError(err error) error { - if err != nil && (err.Error() == api.ErrNotFound.Error() || err.Error() == api.ErrNotImplemented.Error()) { - return agents.ErrNotFound - } - return err -} - -// Album-related methods - -func (w *wasmMediaAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) { - res, err := callMethod(ctx, w, "GetAlbumInfo", func(inst api.MetadataAgent) (*api.AlbumInfoResponse, error) { - return inst.GetAlbumInfo(ctx, &api.AlbumInfoRequest{Name: name, Artist: artist, Mbid: mbid}) - }) - if err != nil { - return nil, w.mapError(err) - } - if res == nil || res.Info == nil { - return nil, agents.ErrNotFound - } - info := res.Info - return &agents.AlbumInfo{ - Name: info.Name, - MBID: info.Mbid, - Description: info.Description, - URL: info.Url, - }, nil -} - -func (w *wasmMediaAgent) GetAlbumImages(ctx context.Context, name, artist, mbid string) ([]agents.ExternalImage, error) { - res, err := callMethod(ctx, w, "GetAlbumImages", func(inst api.MetadataAgent) (*api.AlbumImagesResponse, error) { - return inst.GetAlbumImages(ctx, &api.AlbumImagesRequest{Name: name, Artist: artist, Mbid: mbid}) - }) - if err != nil { - return nil, w.mapError(err) - } - return convertExternalImages(res.Images), nil -} - -// Artist-related methods - -func (w *wasmMediaAgent) GetArtistMBID(ctx context.Context, id string, name string) (string, error) { - res, err := callMethod(ctx, w, "GetArtistMBID", func(inst api.MetadataAgent) (*api.ArtistMBIDResponse, error) { - return inst.GetArtistMBID(ctx, &api.ArtistMBIDRequest{Id: id, Name: name}) - }) - if err != nil { - return "", w.mapError(err) - } - return res.GetMbid(), nil -} - -func (w *wasmMediaAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) { - res, err := callMethod(ctx, w, "GetArtistURL", func(inst api.MetadataAgent) (*api.ArtistURLResponse, error) { - return inst.GetArtistURL(ctx, &api.ArtistURLRequest{Id: id, Name: name, Mbid: mbid}) - }) - if err != nil { - return "", w.mapError(err) - } - return res.GetUrl(), nil -} - -func (w *wasmMediaAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { - res, err := callMethod(ctx, w, "GetArtistBiography", func(inst api.MetadataAgent) (*api.ArtistBiographyResponse, error) { - return inst.GetArtistBiography(ctx, &api.ArtistBiographyRequest{Id: id, Name: name, Mbid: mbid}) - }) - if err != nil { - return "", w.mapError(err) - } - return res.GetBiography(), nil -} - -func (w *wasmMediaAgent) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) { - resp, err := callMethod(ctx, w, "GetSimilarArtists", func(inst api.MetadataAgent) (*api.ArtistSimilarResponse, error) { - return inst.GetSimilarArtists(ctx, &api.ArtistSimilarRequest{Id: id, Name: name, Mbid: mbid, Limit: int32(limit)}) - }) - if err != nil { - return nil, w.mapError(err) - } - artists := make([]agents.Artist, 0, len(resp.GetArtists())) - for _, a := range resp.GetArtists() { - artists = append(artists, agents.Artist{ - Name: a.GetName(), - MBID: a.GetMbid(), - }) - } - return artists, nil -} - -func (w *wasmMediaAgent) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) { - resp, err := callMethod(ctx, w, "GetArtistImages", func(inst api.MetadataAgent) (*api.ArtistImageResponse, error) { - return inst.GetArtistImages(ctx, &api.ArtistImageRequest{Id: id, Name: name, Mbid: mbid}) - }) - if err != nil { - return nil, w.mapError(err) - } - return convertExternalImages(resp.Images), nil -} - -func (w *wasmMediaAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) { - resp, err := callMethod(ctx, w, "GetArtistTopSongs", func(inst api.MetadataAgent) (*api.ArtistTopSongsResponse, error) { - return inst.GetArtistTopSongs(ctx, &api.ArtistTopSongsRequest{Id: id, ArtistName: artistName, Mbid: mbid, Count: int32(count)}) - }) - if err != nil { - return nil, w.mapError(err) - } - songs := make([]agents.Song, 0, len(resp.GetSongs())) - for _, s := range resp.GetSongs() { - songs = append(songs, agents.Song{ - Name: s.GetName(), - MBID: s.GetMbid(), - }) - } - return songs, nil -} - -// Helper function to convert ExternalImage objects from the API to the agents package -func convertExternalImages(images []*api.ExternalImage) []agents.ExternalImage { - result := make([]agents.ExternalImage, 0, len(images)) - for _, img := range images { - result = append(result, agents.ExternalImage{ - URL: img.GetUrl(), - Size: int(img.GetSize()), - }) - } - return result -} diff --git a/plugins/adapter_media_agent_test.go b/plugins/adapter_media_agent_test.go deleted file mode 100644 index e04baf832..000000000 --- a/plugins/adapter_media_agent_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package plugins - -import ( - "context" - "errors" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/conf/configtest" - "github.com/navidrome/navidrome/core/agents" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/api" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Adapter Media Agent", func() { - var ctx context.Context - var mgr *managerImpl - - BeforeEach(func() { - ctx = GinkgoT().Context() - - // Ensure plugins folder is set to testdata - DeferCleanup(configtest.SetupConfig()) - conf.Server.Plugins.Folder = testDataDir - conf.Server.DevPluginCompilationTimeout = 2 * time.Minute - - mgr = createManager(nil, metrics.NewNoopInstance()) - mgr.ScanPlugins() - - // Wait for all plugins to compile to avoid race conditions - err := mgr.EnsureCompiled("multi_plugin") - Expect(err).NotTo(HaveOccurred(), "multi_plugin should compile successfully") - err = mgr.EnsureCompiled("fake_album_agent") - Expect(err).NotTo(HaveOccurred(), "fake_album_agent should compile successfully") - }) - - Describe("AgentName and PluginName", func() { - It("should return the plugin name", func() { - agent := mgr.LoadPlugin("multi_plugin", "MetadataAgent") - Expect(agent).NotTo(BeNil(), "multi_plugin should be loaded") - Expect(agent.PluginID()).To(Equal("multi_plugin")) - }) - It("should return the agent name", func() { - agent, ok := mgr.LoadMediaAgent("multi_plugin") - Expect(ok).To(BeTrue(), "multi_plugin should be loaded as media agent") - Expect(agent.AgentName()).To(Equal("multi_plugin")) - }) - }) - - Describe("Album methods", func() { - var agent *wasmMediaAgent - - BeforeEach(func() { - a, ok := mgr.LoadMediaAgent("fake_album_agent") - Expect(ok).To(BeTrue(), "fake_album_agent should be loaded") - agent = a.(*wasmMediaAgent) - }) - - Context("GetAlbumInfo", func() { - It("should return album information", func() { - info, err := agent.GetAlbumInfo(ctx, "Test Album", "Test Artist", "mbid") - - Expect(err).NotTo(HaveOccurred()) - Expect(info).NotTo(BeNil()) - Expect(info.Name).To(Equal("Test Album")) - Expect(info.MBID).To(Equal("album-mbid-123")) - Expect(info.Description).To(Equal("This is a test album description")) - Expect(info.URL).To(Equal("https://example.com/album")) - }) - - It("should return ErrNotFound when plugin returns not found", func() { - _, err := agent.GetAlbumInfo(ctx, "Test Album", "", "mbid") - - Expect(err).To(Equal(agents.ErrNotFound)) - }) - - It("should return ErrNotFound when plugin returns nil response", func() { - _, err := agent.GetAlbumInfo(ctx, "", "", "") - - Expect(err).To(Equal(agents.ErrNotFound)) - }) - }) - - Context("GetAlbumImages", func() { - It("should return album images", func() { - images, err := agent.GetAlbumImages(ctx, "Test Album", "Test Artist", "mbid") - - Expect(err).NotTo(HaveOccurred()) - Expect(images).To(Equal([]agents.ExternalImage{ - {URL: "https://example.com/album1.jpg", Size: 300}, - {URL: "https://example.com/album2.jpg", Size: 400}, - })) - }) - }) - }) - - Describe("Artist methods", func() { - var agent *wasmMediaAgent - - BeforeEach(func() { - a, ok := mgr.LoadMediaAgent("fake_artist_agent") - Expect(ok).To(BeTrue(), "fake_artist_agent should be loaded") - agent = a.(*wasmMediaAgent) - }) - - Context("GetArtistMBID", func() { - It("should return artist MBID", func() { - mbid, err := agent.GetArtistMBID(ctx, "artist-id", "Test Artist") - - Expect(err).NotTo(HaveOccurred()) - Expect(mbid).To(Equal("1234567890")) - }) - - It("should return ErrNotFound when plugin returns not found", func() { - _, err := agent.GetArtistMBID(ctx, "artist-id", "") - - Expect(err).To(Equal(agents.ErrNotFound)) - }) - }) - - Context("GetArtistURL", func() { - It("should return artist URL", func() { - url, err := agent.GetArtistURL(ctx, "artist-id", "Test Artist", "mbid") - - Expect(err).NotTo(HaveOccurred()) - Expect(url).To(Equal("https://example.com")) - }) - }) - - Context("GetArtistBiography", func() { - It("should return artist biography", func() { - bio, err := agent.GetArtistBiography(ctx, "artist-id", "Test Artist", "mbid") - - Expect(err).NotTo(HaveOccurred()) - Expect(bio).To(Equal("This is a test biography")) - }) - }) - - Context("GetSimilarArtists", func() { - It("should return similar artists", func() { - artists, err := agent.GetSimilarArtists(ctx, "artist-id", "Test Artist", "mbid", 10) - - Expect(err).NotTo(HaveOccurred()) - Expect(artists).To(Equal([]agents.Artist{ - {Name: "Similar Artist 1", MBID: "mbid1"}, - {Name: "Similar Artist 2", MBID: "mbid2"}, - })) - }) - }) - - Context("GetArtistImages", func() { - It("should return artist images", func() { - images, err := agent.GetArtistImages(ctx, "artist-id", "Test Artist", "mbid") - - Expect(err).NotTo(HaveOccurred()) - Expect(images).To(Equal([]agents.ExternalImage{ - {URL: "https://example.com/image1.jpg", Size: 100}, - {URL: "https://example.com/image2.jpg", Size: 200}, - })) - }) - }) - - Context("GetArtistTopSongs", func() { - It("should return artist top songs", func() { - songs, err := agent.GetArtistTopSongs(ctx, "artist-id", "Test Artist", "mbid", 10) - - Expect(err).NotTo(HaveOccurred()) - Expect(songs).To(Equal([]agents.Song{ - {Name: "Song 1", MBID: "mbid1"}, - {Name: "Song 2", MBID: "mbid2"}, - })) - }) - }) - }) - - Describe("Helper functions", func() { - It("convertExternalImages should convert API image objects to agent image objects", func() { - apiImages := []*api.ExternalImage{ - {Url: "https://example.com/image1.jpg", Size: 100}, - {Url: "https://example.com/image2.jpg", Size: 200}, - } - - agentImages := convertExternalImages(apiImages) - Expect(agentImages).To(HaveLen(2)) - - for i, img := range agentImages { - Expect(img.URL).To(Equal(apiImages[i].Url)) - Expect(img.Size).To(Equal(int(apiImages[i].Size))) - } - }) - - It("convertExternalImages should handle empty slice", func() { - agentImages := convertExternalImages([]*api.ExternalImage{}) - Expect(agentImages).To(BeEmpty()) - }) - - It("convertExternalImages should handle nil", func() { - agentImages := convertExternalImages(nil) - Expect(agentImages).To(BeEmpty()) - }) - }) - - Describe("Error mapping", func() { - var agent wasmMediaAgent - - It("should map API ErrNotFound to agents.ErrNotFound", func() { - err := agent.mapError(api.ErrNotFound) - Expect(err).To(Equal(agents.ErrNotFound)) - }) - - It("should map API ErrNotImplemented to agents.ErrNotFound", func() { - err := agent.mapError(api.ErrNotImplemented) - Expect(err).To(Equal(agents.ErrNotFound)) - }) - - It("should pass through other errors", func() { - testErr := errors.New("test error") - err := agent.mapError(testErr) - Expect(err).To(Equal(testErr)) - }) - - It("should handle nil error", func() { - err := agent.mapError(nil) - Expect(err).To(BeNil()) - }) - }) -}) diff --git a/plugins/adapter_scheduler_callback.go b/plugins/adapter_scheduler_callback.go deleted file mode 100644 index 64b7eefff..000000000 --- a/plugins/adapter_scheduler_callback.go +++ /dev/null @@ -1,46 +0,0 @@ -package plugins - -import ( - "context" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/api" - "github.com/tetratelabs/wazero" -) - -// newWasmSchedulerCallback creates a new adapter for a SchedulerCallback plugin -func newWasmSchedulerCallback(wasmPath, pluginID string, m *managerImpl, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin { - loader, err := api.NewSchedulerCallbackPlugin(context.Background(), api.WazeroRuntime(runtime), api.WazeroModuleConfig(mc)) - if err != nil { - log.Error("Error creating scheduler callback plugin", "plugin", pluginID, "path", wasmPath, err) - return nil - } - return &wasmSchedulerCallback{ - baseCapability: newBaseCapability[api.SchedulerCallback, *api.SchedulerCallbackPlugin]( - wasmPath, - pluginID, - CapabilitySchedulerCallback, - m.metrics, - loader, - func(ctx context.Context, l *api.SchedulerCallbackPlugin, path string) (api.SchedulerCallback, error) { - return l.Load(ctx, path) - }, - ), - } -} - -// wasmSchedulerCallback adapts a SchedulerCallback plugin -type wasmSchedulerCallback struct { - *baseCapability[api.SchedulerCallback, *api.SchedulerCallbackPlugin] -} - -func (w *wasmSchedulerCallback) OnSchedulerCallback(ctx context.Context, scheduleID string, payload []byte, isRecurring bool) error { - _, err := callMethod(ctx, w, "OnSchedulerCallback", func(inst api.SchedulerCallback) (*api.SchedulerCallbackResponse, error) { - return inst.OnSchedulerCallback(ctx, &api.SchedulerCallbackRequest{ - ScheduleId: scheduleID, - Payload: payload, - IsRecurring: isRecurring, - }) - }) - return err -} diff --git a/plugins/adapter_scrobbler.go b/plugins/adapter_scrobbler.go deleted file mode 100644 index 54c6af127..000000000 --- a/plugins/adapter_scrobbler.go +++ /dev/null @@ -1,136 +0,0 @@ -package plugins - -import ( - "context" - "time" - - "github.com/navidrome/navidrome/core/scrobbler" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/model/request" - "github.com/navidrome/navidrome/plugins/api" - "github.com/tetratelabs/wazero" -) - -func newWasmScrobblerPlugin(wasmPath, pluginID string, m *managerImpl, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin { - loader, err := api.NewScrobblerPlugin(context.Background(), api.WazeroRuntime(runtime), api.WazeroModuleConfig(mc)) - if err != nil { - log.Error("Error creating scrobbler service plugin", "plugin", pluginID, "path", wasmPath, err) - return nil - } - return &wasmScrobblerPlugin{ - baseCapability: newBaseCapability[api.Scrobbler, *api.ScrobblerPlugin]( - wasmPath, - pluginID, - CapabilityScrobbler, - m.metrics, - loader, - func(ctx context.Context, l *api.ScrobblerPlugin, path string) (api.Scrobbler, error) { - return l.Load(ctx, path) - }, - ), - } -} - -type wasmScrobblerPlugin struct { - *baseCapability[api.Scrobbler, *api.ScrobblerPlugin] -} - -func (w *wasmScrobblerPlugin) IsAuthorized(ctx context.Context, userId string) bool { - username, _ := request.UsernameFrom(ctx) - if username == "" { - u, ok := request.UserFrom(ctx) - if ok { - username = u.UserName - } - } - resp, err := callMethod(ctx, w, "IsAuthorized", func(inst api.Scrobbler) (*api.ScrobblerIsAuthorizedResponse, error) { - return inst.IsAuthorized(ctx, &api.ScrobblerIsAuthorizedRequest{ - UserId: userId, - Username: username, - }) - }) - if err != nil { - log.Warn("Error calling IsAuthorized", "userId", userId, "pluginID", w.id, err) - } - return err == nil && resp.Authorized -} - -func (w *wasmScrobblerPlugin) NowPlaying(ctx context.Context, userId string, track *model.MediaFile, position int) error { - username, _ := request.UsernameFrom(ctx) - if username == "" { - u, ok := request.UserFrom(ctx) - if ok { - username = u.UserName - } - } - - trackInfo := w.toTrackInfo(track, position) - _, err := callMethod(ctx, w, "NowPlaying", func(inst api.Scrobbler) (struct{}, error) { - resp, err := inst.NowPlaying(ctx, &api.ScrobblerNowPlayingRequest{ - UserId: userId, - Username: username, - Track: trackInfo, - Timestamp: time.Now().Unix(), - }) - if err != nil { - return struct{}{}, err - } - if resp.Error != "" { - return struct{}{}, nil - } - return struct{}{}, nil - }) - return err -} - -func (w *wasmScrobblerPlugin) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error { - username, _ := request.UsernameFrom(ctx) - if username == "" { - u, ok := request.UserFrom(ctx) - if ok { - username = u.UserName - } - } - trackInfo := w.toTrackInfo(&s.MediaFile, 0) - _, err := callMethod(ctx, w, "Scrobble", func(inst api.Scrobbler) (struct{}, error) { - resp, err := inst.Scrobble(ctx, &api.ScrobblerScrobbleRequest{ - UserId: userId, - Username: username, - Track: trackInfo, - Timestamp: s.TimeStamp.Unix(), - }) - if err != nil { - return struct{}{}, err - } - if resp.Error != "" { - return struct{}{}, nil - } - return struct{}{}, nil - }) - return err -} - -func (w *wasmScrobblerPlugin) toTrackInfo(track *model.MediaFile, position int) *api.TrackInfo { - artists := make([]*api.Artist, 0, len(track.Participants[model.RoleArtist])) - - for _, a := range track.Participants[model.RoleArtist] { - artists = append(artists, &api.Artist{Name: a.Name, Mbid: a.MbzArtistID}) - } - albumArtists := make([]*api.Artist, 0, len(track.Participants[model.RoleAlbumArtist])) - for _, a := range track.Participants[model.RoleAlbumArtist] { - albumArtists = append(albumArtists, &api.Artist{Name: a.Name, Mbid: a.MbzArtistID}) - } - trackInfo := &api.TrackInfo{ - Id: track.ID, - Mbid: track.MbzRecordingID, - Name: track.Title, - Album: track.Album, - AlbumMbid: track.MbzAlbumID, - Artists: artists, - AlbumArtists: albumArtists, - Length: int32(track.Duration), - Position: int32(position), - } - return trackInfo -} diff --git a/plugins/adapter_websocket_callback.go b/plugins/adapter_websocket_callback.go deleted file mode 100644 index 83b8dd567..000000000 --- a/plugins/adapter_websocket_callback.go +++ /dev/null @@ -1,35 +0,0 @@ -package plugins - -import ( - "context" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/api" - "github.com/tetratelabs/wazero" -) - -// newWasmWebSocketCallback creates a new adapter for a WebSocketCallback plugin -func newWasmWebSocketCallback(wasmPath, pluginID string, m *managerImpl, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin { - loader, err := api.NewWebSocketCallbackPlugin(context.Background(), api.WazeroRuntime(runtime), api.WazeroModuleConfig(mc)) - if err != nil { - log.Error("Error creating WebSocket callback plugin", "plugin", pluginID, "path", wasmPath, err) - return nil - } - return &wasmWebSocketCallback{ - baseCapability: newBaseCapability[api.WebSocketCallback, *api.WebSocketCallbackPlugin]( - wasmPath, - pluginID, - CapabilityWebSocketCallback, - m.metrics, - loader, - func(ctx context.Context, l *api.WebSocketCallbackPlugin, path string) (api.WebSocketCallback, error) { - return l.Load(ctx, path) - }, - ), - } -} - -// wasmWebSocketCallback adapts a WebSocketCallback plugin -type wasmWebSocketCallback struct { - *baseCapability[api.WebSocketCallback, *api.WebSocketCallbackPlugin] -} diff --git a/plugins/api/api.pb.go b/plugins/api/api.pb.go deleted file mode 100644 index b570d5c61..000000000 --- a/plugins/api/api.pb.go +++ /dev/null @@ -1,1136 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: api/api.proto - -package api - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ArtistMBIDRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` -} - -func (x *ArtistMBIDRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistMBIDRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ArtistMBIDRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type ArtistMBIDResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Mbid string `protobuf:"bytes,1,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *ArtistMBIDResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistMBIDResponse) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type ArtistURLRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *ArtistURLRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistURLRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ArtistURLRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ArtistURLRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type ArtistURLResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` -} - -func (x *ArtistURLResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistURLResponse) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -type ArtistBiographyRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *ArtistBiographyRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistBiographyRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ArtistBiographyRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ArtistBiographyRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type ArtistBiographyResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Biography string `protobuf:"bytes,1,opt,name=biography,proto3" json:"biography,omitempty"` -} - -func (x *ArtistBiographyResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistBiographyResponse) GetBiography() string { - if x != nil { - return x.Biography - } - return "" -} - -type ArtistSimilarRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` - Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` -} - -func (x *ArtistSimilarRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistSimilarRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ArtistSimilarRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ArtistSimilarRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -func (x *ArtistSimilarRequest) GetLimit() int32 { - if x != nil { - return x.Limit - } - return 0 -} - -type Artist struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,2,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *Artist) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *Artist) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Artist) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type ArtistSimilarResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Artists []*Artist `protobuf:"bytes,1,rep,name=artists,proto3" json:"artists,omitempty"` -} - -func (x *ArtistSimilarResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistSimilarResponse) GetArtists() []*Artist { - if x != nil { - return x.Artists - } - return nil -} - -type ArtistImageRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *ArtistImageRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistImageRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ArtistImageRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ArtistImageRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type ExternalImage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` -} - -func (x *ExternalImage) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ExternalImage) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -func (x *ExternalImage) GetSize() int32 { - if x != nil { - return x.Size - } - return 0 -} - -type ArtistImageResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Images []*ExternalImage `protobuf:"bytes,1,rep,name=images,proto3" json:"images,omitempty"` -} - -func (x *ArtistImageResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistImageResponse) GetImages() []*ExternalImage { - if x != nil { - return x.Images - } - return nil -} - -type ArtistTopSongsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - ArtistName string `protobuf:"bytes,2,opt,name=artistName,proto3" json:"artistName,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` - Count int32 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"` -} - -func (x *ArtistTopSongsRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistTopSongsRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ArtistTopSongsRequest) GetArtistName() string { - if x != nil { - return x.ArtistName - } - return "" -} - -func (x *ArtistTopSongsRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -func (x *ArtistTopSongsRequest) GetCount() int32 { - if x != nil { - return x.Count - } - return 0 -} - -type Song struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,2,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *Song) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *Song) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Song) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type ArtistTopSongsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Songs []*Song `protobuf:"bytes,1,rep,name=songs,proto3" json:"songs,omitempty"` -} - -func (x *ArtistTopSongsResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ArtistTopSongsResponse) GetSongs() []*Song { - if x != nil { - return x.Songs - } - return nil -} - -type AlbumInfoRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Artist string `protobuf:"bytes,2,opt,name=artist,proto3" json:"artist,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *AlbumInfoRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *AlbumInfoRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *AlbumInfoRequest) GetArtist() string { - if x != nil { - return x.Artist - } - return "" -} - -func (x *AlbumInfoRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type AlbumInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Mbid string `protobuf:"bytes,2,opt,name=mbid,proto3" json:"mbid,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"` -} - -func (x *AlbumInfo) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *AlbumInfo) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *AlbumInfo) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -func (x *AlbumInfo) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *AlbumInfo) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -type AlbumInfoResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Info *AlbumInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` -} - -func (x *AlbumInfoResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *AlbumInfoResponse) GetInfo() *AlbumInfo { - if x != nil { - return x.Info - } - return nil -} - -type AlbumImagesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Artist string `protobuf:"bytes,2,opt,name=artist,proto3" json:"artist,omitempty"` - Mbid string `protobuf:"bytes,3,opt,name=mbid,proto3" json:"mbid,omitempty"` -} - -func (x *AlbumImagesRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *AlbumImagesRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *AlbumImagesRequest) GetArtist() string { - if x != nil { - return x.Artist - } - return "" -} - -func (x *AlbumImagesRequest) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -type AlbumImagesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Images []*ExternalImage `protobuf:"bytes,1,rep,name=images,proto3" json:"images,omitempty"` -} - -func (x *AlbumImagesResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *AlbumImagesResponse) GetImages() []*ExternalImage { - if x != nil { - return x.Images - } - return nil -} - -type ScrobblerIsAuthorizedRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` -} - -func (x *ScrobblerIsAuthorizedRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScrobblerIsAuthorizedRequest) GetUserId() string { - if x != nil { - return x.UserId - } - return "" -} - -func (x *ScrobblerIsAuthorizedRequest) GetUsername() string { - if x != nil { - return x.Username - } - return "" -} - -type ScrobblerIsAuthorizedResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *ScrobblerIsAuthorizedResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScrobblerIsAuthorizedResponse) GetAuthorized() bool { - if x != nil { - return x.Authorized - } - return false -} - -func (x *ScrobblerIsAuthorizedResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type TrackInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Mbid string `protobuf:"bytes,2,opt,name=mbid,proto3" json:"mbid,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - Album string `protobuf:"bytes,4,opt,name=album,proto3" json:"album,omitempty"` - AlbumMbid string `protobuf:"bytes,5,opt,name=album_mbid,json=albumMbid,proto3" json:"album_mbid,omitempty"` - Artists []*Artist `protobuf:"bytes,6,rep,name=artists,proto3" json:"artists,omitempty"` - AlbumArtists []*Artist `protobuf:"bytes,7,rep,name=album_artists,json=albumArtists,proto3" json:"album_artists,omitempty"` - Length int32 `protobuf:"varint,8,opt,name=length,proto3" json:"length,omitempty"` // seconds - Position int32 `protobuf:"varint,9,opt,name=position,proto3" json:"position,omitempty"` // seconds -} - -func (x *TrackInfo) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *TrackInfo) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *TrackInfo) GetMbid() string { - if x != nil { - return x.Mbid - } - return "" -} - -func (x *TrackInfo) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TrackInfo) GetAlbum() string { - if x != nil { - return x.Album - } - return "" -} - -func (x *TrackInfo) GetAlbumMbid() string { - if x != nil { - return x.AlbumMbid - } - return "" -} - -func (x *TrackInfo) GetArtists() []*Artist { - if x != nil { - return x.Artists - } - return nil -} - -func (x *TrackInfo) GetAlbumArtists() []*Artist { - if x != nil { - return x.AlbumArtists - } - return nil -} - -func (x *TrackInfo) GetLength() int32 { - if x != nil { - return x.Length - } - return 0 -} - -func (x *TrackInfo) GetPosition() int32 { - if x != nil { - return x.Position - } - return 0 -} - -type ScrobblerNowPlayingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` - Track *TrackInfo `protobuf:"bytes,3,opt,name=track,proto3" json:"track,omitempty"` - Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` -} - -func (x *ScrobblerNowPlayingRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScrobblerNowPlayingRequest) GetUserId() string { - if x != nil { - return x.UserId - } - return "" -} - -func (x *ScrobblerNowPlayingRequest) GetUsername() string { - if x != nil { - return x.Username - } - return "" -} - -func (x *ScrobblerNowPlayingRequest) GetTrack() *TrackInfo { - if x != nil { - return x.Track - } - return nil -} - -func (x *ScrobblerNowPlayingRequest) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -type ScrobblerNowPlayingResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *ScrobblerNowPlayingResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScrobblerNowPlayingResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type ScrobblerScrobbleRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` - Track *TrackInfo `protobuf:"bytes,3,opt,name=track,proto3" json:"track,omitempty"` - Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` -} - -func (x *ScrobblerScrobbleRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScrobblerScrobbleRequest) GetUserId() string { - if x != nil { - return x.UserId - } - return "" -} - -func (x *ScrobblerScrobbleRequest) GetUsername() string { - if x != nil { - return x.Username - } - return "" -} - -func (x *ScrobblerScrobbleRequest) GetTrack() *TrackInfo { - if x != nil { - return x.Track - } - return nil -} - -func (x *ScrobblerScrobbleRequest) GetTimestamp() int64 { - if x != nil { - return x.Timestamp - } - return 0 -} - -type ScrobblerScrobbleResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *ScrobblerScrobbleResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScrobblerScrobbleResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type SchedulerCallbackRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ScheduleId string `protobuf:"bytes,1,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` // ID of the scheduled job that triggered this callback - Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // The data passed when the job was scheduled - IsRecurring bool `protobuf:"varint,3,opt,name=is_recurring,json=isRecurring,proto3" json:"is_recurring,omitempty"` // Whether this is from a recurring schedule (cron job) -} - -func (x *SchedulerCallbackRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SchedulerCallbackRequest) GetScheduleId() string { - if x != nil { - return x.ScheduleId - } - return "" -} - -func (x *SchedulerCallbackRequest) GetPayload() []byte { - if x != nil { - return x.Payload - } - return nil -} - -func (x *SchedulerCallbackRequest) GetIsRecurring() bool { - if x != nil { - return x.IsRecurring - } - return false -} - -type SchedulerCallbackResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` // Error message if the callback failed -} - -func (x *SchedulerCallbackResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SchedulerCallbackResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type InitRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Config map[string]string `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Configuration specific to this plugin -} - -func (x *InitRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *InitRequest) GetConfig() map[string]string { - if x != nil { - return x.Config - } - return nil -} - -type InitResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` // Error message if initialization failed -} - -func (x *InitResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *InitResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type OnTextMessageRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *OnTextMessageRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *OnTextMessageRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *OnTextMessageRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type OnTextMessageResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *OnTextMessageResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -type OnBinaryMessageRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` -} - -func (x *OnBinaryMessageRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *OnBinaryMessageRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *OnBinaryMessageRequest) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -type OnBinaryMessageResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *OnBinaryMessageResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -type OnErrorRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *OnErrorRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *OnErrorRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *OnErrorRequest) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type OnErrorResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *OnErrorResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -type OnCloseRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Code int32 `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"` - Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` -} - -func (x *OnCloseRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *OnCloseRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *OnCloseRequest) GetCode() int32 { - if x != nil { - return x.Code - } - return 0 -} - -func (x *OnCloseRequest) GetReason() string { - if x != nil { - return x.Reason - } - return "" -} - -type OnCloseResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *OnCloseResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -// go:plugin type=plugin version=1 -type MetadataAgent interface { - // Artist metadata methods - GetArtistMBID(context.Context, *ArtistMBIDRequest) (*ArtistMBIDResponse, error) - GetArtistURL(context.Context, *ArtistURLRequest) (*ArtistURLResponse, error) - GetArtistBiography(context.Context, *ArtistBiographyRequest) (*ArtistBiographyResponse, error) - GetSimilarArtists(context.Context, *ArtistSimilarRequest) (*ArtistSimilarResponse, error) - GetArtistImages(context.Context, *ArtistImageRequest) (*ArtistImageResponse, error) - GetArtistTopSongs(context.Context, *ArtistTopSongsRequest) (*ArtistTopSongsResponse, error) - // Album metadata methods - GetAlbumInfo(context.Context, *AlbumInfoRequest) (*AlbumInfoResponse, error) - GetAlbumImages(context.Context, *AlbumImagesRequest) (*AlbumImagesResponse, error) -} - -// go:plugin type=plugin version=1 -type Scrobbler interface { - IsAuthorized(context.Context, *ScrobblerIsAuthorizedRequest) (*ScrobblerIsAuthorizedResponse, error) - NowPlaying(context.Context, *ScrobblerNowPlayingRequest) (*ScrobblerNowPlayingResponse, error) - Scrobble(context.Context, *ScrobblerScrobbleRequest) (*ScrobblerScrobbleResponse, error) -} - -// go:plugin type=plugin version=1 -type SchedulerCallback interface { - OnSchedulerCallback(context.Context, *SchedulerCallbackRequest) (*SchedulerCallbackResponse, error) -} - -// go:plugin type=plugin version=1 -type LifecycleManagement interface { - OnInit(context.Context, *InitRequest) (*InitResponse, error) -} - -// go:plugin type=plugin version=1 -type WebSocketCallback interface { - // Called when a text message is received - OnTextMessage(context.Context, *OnTextMessageRequest) (*OnTextMessageResponse, error) - // Called when a binary message is received - OnBinaryMessage(context.Context, *OnBinaryMessageRequest) (*OnBinaryMessageResponse, error) - // Called when an error occurs - OnError(context.Context, *OnErrorRequest) (*OnErrorResponse, error) - // Called when the connection is closed - OnClose(context.Context, *OnCloseRequest) (*OnCloseResponse, error) -} diff --git a/plugins/api/api.proto b/plugins/api/api.proto deleted file mode 100644 index 7929ff9e6..000000000 --- a/plugins/api/api.proto +++ /dev/null @@ -1,246 +0,0 @@ -syntax = "proto3"; - -package api; - -option go_package = "github.com/navidrome/navidrome/plugins/api;api"; - -// go:plugin type=plugin version=1 -service MetadataAgent { - // Artist metadata methods - rpc GetArtistMBID(ArtistMBIDRequest) returns (ArtistMBIDResponse); - rpc GetArtistURL(ArtistURLRequest) returns (ArtistURLResponse); - rpc GetArtistBiography(ArtistBiographyRequest) returns (ArtistBiographyResponse); - rpc GetSimilarArtists(ArtistSimilarRequest) returns (ArtistSimilarResponse); - rpc GetArtistImages(ArtistImageRequest) returns (ArtistImageResponse); - rpc GetArtistTopSongs(ArtistTopSongsRequest) returns (ArtistTopSongsResponse); - - // Album metadata methods - rpc GetAlbumInfo(AlbumInfoRequest) returns (AlbumInfoResponse); - rpc GetAlbumImages(AlbumImagesRequest) returns (AlbumImagesResponse); -} - -message ArtistMBIDRequest { - string id = 1; - string name = 2; -} - -message ArtistMBIDResponse { - string mbid = 1; -} - -message ArtistURLRequest { - string id = 1; - string name = 2; - string mbid = 3; -} - -message ArtistURLResponse { - string url = 1; -} - -message ArtistBiographyRequest { - string id = 1; - string name = 2; - string mbid = 3; -} - -message ArtistBiographyResponse { - string biography = 1; -} - -message ArtistSimilarRequest { - string id = 1; - string name = 2; - string mbid = 3; - int32 limit = 4; -} - -message Artist { - string name = 1; - string mbid = 2; -} - -message ArtistSimilarResponse { - repeated Artist artists = 1; -} - -message ArtistImageRequest { - string id = 1; - string name = 2; - string mbid = 3; -} - -message ExternalImage { - string url = 1; - int32 size = 2; -} - -message ArtistImageResponse { - repeated ExternalImage images = 1; -} - -message ArtistTopSongsRequest { - string id = 1; - string artistName = 2; - string mbid = 3; - int32 count = 4; -} - -message Song { - string name = 1; - string mbid = 2; -} - -message ArtistTopSongsResponse { - repeated Song songs = 1; -} - -message AlbumInfoRequest { - string name = 1; - string artist = 2; - string mbid = 3; -} - -message AlbumInfo { - string name = 1; - string mbid = 2; - string description = 3; - string url = 4; -} - -message AlbumInfoResponse { - AlbumInfo info = 1; -} - -message AlbumImagesRequest { - string name = 1; - string artist = 2; - string mbid = 3; -} - -message AlbumImagesResponse { - repeated ExternalImage images = 1; -} - -// go:plugin type=plugin version=1 -service Scrobbler { - rpc IsAuthorized(ScrobblerIsAuthorizedRequest) returns (ScrobblerIsAuthorizedResponse); - rpc NowPlaying(ScrobblerNowPlayingRequest) returns (ScrobblerNowPlayingResponse); - rpc Scrobble(ScrobblerScrobbleRequest) returns (ScrobblerScrobbleResponse); -} - -message ScrobblerIsAuthorizedRequest { - string user_id = 1; - string username = 2; -} - -message ScrobblerIsAuthorizedResponse { - bool authorized = 1; - string error = 2; -} - -message TrackInfo { - string id = 1; - string mbid = 2; - string name = 3; - string album = 4; - string album_mbid = 5; - repeated Artist artists = 6; - repeated Artist album_artists = 7; - int32 length = 8; // seconds - int32 position = 9; // seconds -} - -message ScrobblerNowPlayingRequest { - string user_id = 1; - string username = 2; - TrackInfo track = 3; - int64 timestamp = 4; -} - -message ScrobblerNowPlayingResponse { - string error = 1; -} - -message ScrobblerScrobbleRequest { - string user_id = 1; - string username = 2; - TrackInfo track = 3; - int64 timestamp = 4; -} - -message ScrobblerScrobbleResponse { - string error = 1; -} - -// go:plugin type=plugin version=1 -service SchedulerCallback { - rpc OnSchedulerCallback(SchedulerCallbackRequest) returns (SchedulerCallbackResponse); -} - -message SchedulerCallbackRequest { - string schedule_id = 1; // ID of the scheduled job that triggered this callback - bytes payload = 2; // The data passed when the job was scheduled - bool is_recurring = 3; // Whether this is from a recurring schedule (cron job) -} - -message SchedulerCallbackResponse { - string error = 1; // Error message if the callback failed -} - -// go:plugin type=plugin version=1 -service LifecycleManagement { - rpc OnInit(InitRequest) returns (InitResponse); -} - -message InitRequest { - map config = 1; // Configuration specific to this plugin -} - -message InitResponse { - string error = 1; // Error message if initialization failed -} - -// go:plugin type=plugin version=1 -service WebSocketCallback { - // Called when a text message is received - rpc OnTextMessage(OnTextMessageRequest) returns (OnTextMessageResponse); - - // Called when a binary message is received - rpc OnBinaryMessage(OnBinaryMessageRequest) returns (OnBinaryMessageResponse); - - // Called when an error occurs - rpc OnError(OnErrorRequest) returns (OnErrorResponse); - - // Called when the connection is closed - rpc OnClose(OnCloseRequest) returns (OnCloseResponse); -} - -message OnTextMessageRequest { - string connection_id = 1; - string message = 2; -} - -message OnTextMessageResponse {} - -message OnBinaryMessageRequest { - string connection_id = 1; - bytes data = 2; -} - -message OnBinaryMessageResponse {} - -message OnErrorRequest { - string connection_id = 1; - string error = 2; -} - -message OnErrorResponse {} - -message OnCloseRequest { - string connection_id = 1; - int32 code = 2; - string reason = 3; -} - -message OnCloseResponse {} \ No newline at end of file diff --git a/plugins/api/api_host.pb.go b/plugins/api/api_host.pb.go deleted file mode 100644 index 55e648c6c..000000000 --- a/plugins/api/api_host.pb.go +++ /dev/null @@ -1,1688 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: api/api.proto - -package api - -import ( - context "context" - errors "errors" - fmt "fmt" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" - sys "github.com/tetratelabs/wazero/sys" - os "os" -) - -const MetadataAgentPluginAPIVersion = 1 - -type MetadataAgentPlugin struct { - newRuntime func(context.Context) (wazero.Runtime, error) - moduleConfig wazero.ModuleConfig -} - -func NewMetadataAgentPlugin(ctx context.Context, opts ...wazeroConfigOption) (*MetadataAgentPlugin, error) { - o := &WazeroConfig{ - newRuntime: DefaultWazeroRuntime(), - moduleConfig: wazero.NewModuleConfig().WithStartFunctions("_initialize"), - } - - for _, opt := range opts { - opt(o) - } - - return &MetadataAgentPlugin{ - newRuntime: o.newRuntime, - moduleConfig: o.moduleConfig, - }, nil -} - -type metadataAgent interface { - Close(ctx context.Context) error - MetadataAgent -} - -func (p *MetadataAgentPlugin) Load(ctx context.Context, pluginPath string) (metadataAgent, error) { - b, err := os.ReadFile(pluginPath) - if err != nil { - return nil, err - } - - // Create a new runtime so that multiple modules will not conflict - r, err := p.newRuntime(ctx) - if err != nil { - return nil, err - } - - // Compile the WebAssembly module using the default configuration. - code, err := r.CompileModule(ctx, b) - if err != nil { - return nil, err - } - - // InstantiateModule runs the "_start" function, WASI's "main". - module, err := r.InstantiateModule(ctx, code, p.moduleConfig) - if err != nil { - // Note: Most compilers do not exit the module after running "_start", - // unless there was an Error. This allows you to call exported functions. - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - return nil, fmt.Errorf("unexpected exit_code: %d", exitErr.ExitCode()) - } else if !ok { - return nil, err - } - } - - // Compare API versions with the loading plugin - apiVersion := module.ExportedFunction("metadata_agent_api_version") - if apiVersion == nil { - return nil, errors.New("metadata_agent_api_version is not exported") - } - results, err := apiVersion.Call(ctx) - if err != nil { - return nil, err - } else if len(results) != 1 { - return nil, errors.New("invalid metadata_agent_api_version signature") - } - if results[0] != MetadataAgentPluginAPIVersion { - return nil, fmt.Errorf("API version mismatch, host: %d, plugin: %d", MetadataAgentPluginAPIVersion, results[0]) - } - - getartistmbid := module.ExportedFunction("metadata_agent_get_artist_mbid") - if getartistmbid == nil { - return nil, errors.New("metadata_agent_get_artist_mbid is not exported") - } - getartisturl := module.ExportedFunction("metadata_agent_get_artist_url") - if getartisturl == nil { - return nil, errors.New("metadata_agent_get_artist_url is not exported") - } - getartistbiography := module.ExportedFunction("metadata_agent_get_artist_biography") - if getartistbiography == nil { - return nil, errors.New("metadata_agent_get_artist_biography is not exported") - } - getsimilarartists := module.ExportedFunction("metadata_agent_get_similar_artists") - if getsimilarartists == nil { - return nil, errors.New("metadata_agent_get_similar_artists is not exported") - } - getartistimages := module.ExportedFunction("metadata_agent_get_artist_images") - if getartistimages == nil { - return nil, errors.New("metadata_agent_get_artist_images is not exported") - } - getartisttopsongs := module.ExportedFunction("metadata_agent_get_artist_top_songs") - if getartisttopsongs == nil { - return nil, errors.New("metadata_agent_get_artist_top_songs is not exported") - } - getalbuminfo := module.ExportedFunction("metadata_agent_get_album_info") - if getalbuminfo == nil { - return nil, errors.New("metadata_agent_get_album_info is not exported") - } - getalbumimages := module.ExportedFunction("metadata_agent_get_album_images") - if getalbumimages == nil { - return nil, errors.New("metadata_agent_get_album_images is not exported") - } - - malloc := module.ExportedFunction("malloc") - if malloc == nil { - return nil, errors.New("malloc is not exported") - } - - free := module.ExportedFunction("free") - if free == nil { - return nil, errors.New("free is not exported") - } - return &metadataAgentPlugin{ - runtime: r, - module: module, - malloc: malloc, - free: free, - getartistmbid: getartistmbid, - getartisturl: getartisturl, - getartistbiography: getartistbiography, - getsimilarartists: getsimilarartists, - getartistimages: getartistimages, - getartisttopsongs: getartisttopsongs, - getalbuminfo: getalbuminfo, - getalbumimages: getalbumimages, - }, nil -} - -func (p *metadataAgentPlugin) Close(ctx context.Context) (err error) { - if r := p.runtime; r != nil { - r.Close(ctx) - } - return -} - -type metadataAgentPlugin struct { - runtime wazero.Runtime - module api.Module - malloc api.Function - free api.Function - getartistmbid api.Function - getartisturl api.Function - getartistbiography api.Function - getsimilarartists api.Function - getartistimages api.Function - getartisttopsongs api.Function - getalbuminfo api.Function - getalbumimages api.Function -} - -func (p *metadataAgentPlugin) GetArtistMBID(ctx context.Context, request *ArtistMBIDRequest) (*ArtistMBIDResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getartistmbid.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ArtistMBIDResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetArtistURL(ctx context.Context, request *ArtistURLRequest) (*ArtistURLResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getartisturl.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ArtistURLResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetArtistBiography(ctx context.Context, request *ArtistBiographyRequest) (*ArtistBiographyResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getartistbiography.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ArtistBiographyResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetSimilarArtists(ctx context.Context, request *ArtistSimilarRequest) (*ArtistSimilarResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getsimilarartists.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ArtistSimilarResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetArtistImages(ctx context.Context, request *ArtistImageRequest) (*ArtistImageResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getartistimages.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ArtistImageResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetArtistTopSongs(ctx context.Context, request *ArtistTopSongsRequest) (*ArtistTopSongsResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getartisttopsongs.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ArtistTopSongsResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetAlbumInfo(ctx context.Context, request *AlbumInfoRequest) (*AlbumInfoResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getalbuminfo.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(AlbumInfoResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *metadataAgentPlugin) GetAlbumImages(ctx context.Context, request *AlbumImagesRequest) (*AlbumImagesResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.getalbumimages.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(AlbumImagesResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} - -const ScrobblerPluginAPIVersion = 1 - -type ScrobblerPlugin struct { - newRuntime func(context.Context) (wazero.Runtime, error) - moduleConfig wazero.ModuleConfig -} - -func NewScrobblerPlugin(ctx context.Context, opts ...wazeroConfigOption) (*ScrobblerPlugin, error) { - o := &WazeroConfig{ - newRuntime: DefaultWazeroRuntime(), - moduleConfig: wazero.NewModuleConfig().WithStartFunctions("_initialize"), - } - - for _, opt := range opts { - opt(o) - } - - return &ScrobblerPlugin{ - newRuntime: o.newRuntime, - moduleConfig: o.moduleConfig, - }, nil -} - -type scrobbler interface { - Close(ctx context.Context) error - Scrobbler -} - -func (p *ScrobblerPlugin) Load(ctx context.Context, pluginPath string) (scrobbler, error) { - b, err := os.ReadFile(pluginPath) - if err != nil { - return nil, err - } - - // Create a new runtime so that multiple modules will not conflict - r, err := p.newRuntime(ctx) - if err != nil { - return nil, err - } - - // Compile the WebAssembly module using the default configuration. - code, err := r.CompileModule(ctx, b) - if err != nil { - return nil, err - } - - // InstantiateModule runs the "_start" function, WASI's "main". - module, err := r.InstantiateModule(ctx, code, p.moduleConfig) - if err != nil { - // Note: Most compilers do not exit the module after running "_start", - // unless there was an Error. This allows you to call exported functions. - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - return nil, fmt.Errorf("unexpected exit_code: %d", exitErr.ExitCode()) - } else if !ok { - return nil, err - } - } - - // Compare API versions with the loading plugin - apiVersion := module.ExportedFunction("scrobbler_api_version") - if apiVersion == nil { - return nil, errors.New("scrobbler_api_version is not exported") - } - results, err := apiVersion.Call(ctx) - if err != nil { - return nil, err - } else if len(results) != 1 { - return nil, errors.New("invalid scrobbler_api_version signature") - } - if results[0] != ScrobblerPluginAPIVersion { - return nil, fmt.Errorf("API version mismatch, host: %d, plugin: %d", ScrobblerPluginAPIVersion, results[0]) - } - - isauthorized := module.ExportedFunction("scrobbler_is_authorized") - if isauthorized == nil { - return nil, errors.New("scrobbler_is_authorized is not exported") - } - nowplaying := module.ExportedFunction("scrobbler_now_playing") - if nowplaying == nil { - return nil, errors.New("scrobbler_now_playing is not exported") - } - scrobble := module.ExportedFunction("scrobbler_scrobble") - if scrobble == nil { - return nil, errors.New("scrobbler_scrobble is not exported") - } - - malloc := module.ExportedFunction("malloc") - if malloc == nil { - return nil, errors.New("malloc is not exported") - } - - free := module.ExportedFunction("free") - if free == nil { - return nil, errors.New("free is not exported") - } - return &scrobblerPlugin{ - runtime: r, - module: module, - malloc: malloc, - free: free, - isauthorized: isauthorized, - nowplaying: nowplaying, - scrobble: scrobble, - }, nil -} - -func (p *scrobblerPlugin) Close(ctx context.Context) (err error) { - if r := p.runtime; r != nil { - r.Close(ctx) - } - return -} - -type scrobblerPlugin struct { - runtime wazero.Runtime - module api.Module - malloc api.Function - free api.Function - isauthorized api.Function - nowplaying api.Function - scrobble api.Function -} - -func (p *scrobblerPlugin) IsAuthorized(ctx context.Context, request *ScrobblerIsAuthorizedRequest) (*ScrobblerIsAuthorizedResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.isauthorized.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ScrobblerIsAuthorizedResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *scrobblerPlugin) NowPlaying(ctx context.Context, request *ScrobblerNowPlayingRequest) (*ScrobblerNowPlayingResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.nowplaying.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ScrobblerNowPlayingResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *scrobblerPlugin) Scrobble(ctx context.Context, request *ScrobblerScrobbleRequest) (*ScrobblerScrobbleResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.scrobble.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(ScrobblerScrobbleResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} - -const SchedulerCallbackPluginAPIVersion = 1 - -type SchedulerCallbackPlugin struct { - newRuntime func(context.Context) (wazero.Runtime, error) - moduleConfig wazero.ModuleConfig -} - -func NewSchedulerCallbackPlugin(ctx context.Context, opts ...wazeroConfigOption) (*SchedulerCallbackPlugin, error) { - o := &WazeroConfig{ - newRuntime: DefaultWazeroRuntime(), - moduleConfig: wazero.NewModuleConfig().WithStartFunctions("_initialize"), - } - - for _, opt := range opts { - opt(o) - } - - return &SchedulerCallbackPlugin{ - newRuntime: o.newRuntime, - moduleConfig: o.moduleConfig, - }, nil -} - -type schedulerCallback interface { - Close(ctx context.Context) error - SchedulerCallback -} - -func (p *SchedulerCallbackPlugin) Load(ctx context.Context, pluginPath string) (schedulerCallback, error) { - b, err := os.ReadFile(pluginPath) - if err != nil { - return nil, err - } - - // Create a new runtime so that multiple modules will not conflict - r, err := p.newRuntime(ctx) - if err != nil { - return nil, err - } - - // Compile the WebAssembly module using the default configuration. - code, err := r.CompileModule(ctx, b) - if err != nil { - return nil, err - } - - // InstantiateModule runs the "_start" function, WASI's "main". - module, err := r.InstantiateModule(ctx, code, p.moduleConfig) - if err != nil { - // Note: Most compilers do not exit the module after running "_start", - // unless there was an Error. This allows you to call exported functions. - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - return nil, fmt.Errorf("unexpected exit_code: %d", exitErr.ExitCode()) - } else if !ok { - return nil, err - } - } - - // Compare API versions with the loading plugin - apiVersion := module.ExportedFunction("scheduler_callback_api_version") - if apiVersion == nil { - return nil, errors.New("scheduler_callback_api_version is not exported") - } - results, err := apiVersion.Call(ctx) - if err != nil { - return nil, err - } else if len(results) != 1 { - return nil, errors.New("invalid scheduler_callback_api_version signature") - } - if results[0] != SchedulerCallbackPluginAPIVersion { - return nil, fmt.Errorf("API version mismatch, host: %d, plugin: %d", SchedulerCallbackPluginAPIVersion, results[0]) - } - - onschedulercallback := module.ExportedFunction("scheduler_callback_on_scheduler_callback") - if onschedulercallback == nil { - return nil, errors.New("scheduler_callback_on_scheduler_callback is not exported") - } - - malloc := module.ExportedFunction("malloc") - if malloc == nil { - return nil, errors.New("malloc is not exported") - } - - free := module.ExportedFunction("free") - if free == nil { - return nil, errors.New("free is not exported") - } - return &schedulerCallbackPlugin{ - runtime: r, - module: module, - malloc: malloc, - free: free, - onschedulercallback: onschedulercallback, - }, nil -} - -func (p *schedulerCallbackPlugin) Close(ctx context.Context) (err error) { - if r := p.runtime; r != nil { - r.Close(ctx) - } - return -} - -type schedulerCallbackPlugin struct { - runtime wazero.Runtime - module api.Module - malloc api.Function - free api.Function - onschedulercallback api.Function -} - -func (p *schedulerCallbackPlugin) OnSchedulerCallback(ctx context.Context, request *SchedulerCallbackRequest) (*SchedulerCallbackResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.onschedulercallback.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(SchedulerCallbackResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} - -const LifecycleManagementPluginAPIVersion = 1 - -type LifecycleManagementPlugin struct { - newRuntime func(context.Context) (wazero.Runtime, error) - moduleConfig wazero.ModuleConfig -} - -func NewLifecycleManagementPlugin(ctx context.Context, opts ...wazeroConfigOption) (*LifecycleManagementPlugin, error) { - o := &WazeroConfig{ - newRuntime: DefaultWazeroRuntime(), - moduleConfig: wazero.NewModuleConfig().WithStartFunctions("_initialize"), - } - - for _, opt := range opts { - opt(o) - } - - return &LifecycleManagementPlugin{ - newRuntime: o.newRuntime, - moduleConfig: o.moduleConfig, - }, nil -} - -type lifecycleManagement interface { - Close(ctx context.Context) error - LifecycleManagement -} - -func (p *LifecycleManagementPlugin) Load(ctx context.Context, pluginPath string) (lifecycleManagement, error) { - b, err := os.ReadFile(pluginPath) - if err != nil { - return nil, err - } - - // Create a new runtime so that multiple modules will not conflict - r, err := p.newRuntime(ctx) - if err != nil { - return nil, err - } - - // Compile the WebAssembly module using the default configuration. - code, err := r.CompileModule(ctx, b) - if err != nil { - return nil, err - } - - // InstantiateModule runs the "_start" function, WASI's "main". - module, err := r.InstantiateModule(ctx, code, p.moduleConfig) - if err != nil { - // Note: Most compilers do not exit the module after running "_start", - // unless there was an Error. This allows you to call exported functions. - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - return nil, fmt.Errorf("unexpected exit_code: %d", exitErr.ExitCode()) - } else if !ok { - return nil, err - } - } - - // Compare API versions with the loading plugin - apiVersion := module.ExportedFunction("lifecycle_management_api_version") - if apiVersion == nil { - return nil, errors.New("lifecycle_management_api_version is not exported") - } - results, err := apiVersion.Call(ctx) - if err != nil { - return nil, err - } else if len(results) != 1 { - return nil, errors.New("invalid lifecycle_management_api_version signature") - } - if results[0] != LifecycleManagementPluginAPIVersion { - return nil, fmt.Errorf("API version mismatch, host: %d, plugin: %d", LifecycleManagementPluginAPIVersion, results[0]) - } - - oninit := module.ExportedFunction("lifecycle_management_on_init") - if oninit == nil { - return nil, errors.New("lifecycle_management_on_init is not exported") - } - - malloc := module.ExportedFunction("malloc") - if malloc == nil { - return nil, errors.New("malloc is not exported") - } - - free := module.ExportedFunction("free") - if free == nil { - return nil, errors.New("free is not exported") - } - return &lifecycleManagementPlugin{ - runtime: r, - module: module, - malloc: malloc, - free: free, - oninit: oninit, - }, nil -} - -func (p *lifecycleManagementPlugin) Close(ctx context.Context) (err error) { - if r := p.runtime; r != nil { - r.Close(ctx) - } - return -} - -type lifecycleManagementPlugin struct { - runtime wazero.Runtime - module api.Module - malloc api.Function - free api.Function - oninit api.Function -} - -func (p *lifecycleManagementPlugin) OnInit(ctx context.Context, request *InitRequest) (*InitResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.oninit.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(InitResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} - -const WebSocketCallbackPluginAPIVersion = 1 - -type WebSocketCallbackPlugin struct { - newRuntime func(context.Context) (wazero.Runtime, error) - moduleConfig wazero.ModuleConfig -} - -func NewWebSocketCallbackPlugin(ctx context.Context, opts ...wazeroConfigOption) (*WebSocketCallbackPlugin, error) { - o := &WazeroConfig{ - newRuntime: DefaultWazeroRuntime(), - moduleConfig: wazero.NewModuleConfig().WithStartFunctions("_initialize"), - } - - for _, opt := range opts { - opt(o) - } - - return &WebSocketCallbackPlugin{ - newRuntime: o.newRuntime, - moduleConfig: o.moduleConfig, - }, nil -} - -type webSocketCallback interface { - Close(ctx context.Context) error - WebSocketCallback -} - -func (p *WebSocketCallbackPlugin) Load(ctx context.Context, pluginPath string) (webSocketCallback, error) { - b, err := os.ReadFile(pluginPath) - if err != nil { - return nil, err - } - - // Create a new runtime so that multiple modules will not conflict - r, err := p.newRuntime(ctx) - if err != nil { - return nil, err - } - - // Compile the WebAssembly module using the default configuration. - code, err := r.CompileModule(ctx, b) - if err != nil { - return nil, err - } - - // InstantiateModule runs the "_start" function, WASI's "main". - module, err := r.InstantiateModule(ctx, code, p.moduleConfig) - if err != nil { - // Note: Most compilers do not exit the module after running "_start", - // unless there was an Error. This allows you to call exported functions. - if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { - return nil, fmt.Errorf("unexpected exit_code: %d", exitErr.ExitCode()) - } else if !ok { - return nil, err - } - } - - // Compare API versions with the loading plugin - apiVersion := module.ExportedFunction("web_socket_callback_api_version") - if apiVersion == nil { - return nil, errors.New("web_socket_callback_api_version is not exported") - } - results, err := apiVersion.Call(ctx) - if err != nil { - return nil, err - } else if len(results) != 1 { - return nil, errors.New("invalid web_socket_callback_api_version signature") - } - if results[0] != WebSocketCallbackPluginAPIVersion { - return nil, fmt.Errorf("API version mismatch, host: %d, plugin: %d", WebSocketCallbackPluginAPIVersion, results[0]) - } - - ontextmessage := module.ExportedFunction("web_socket_callback_on_text_message") - if ontextmessage == nil { - return nil, errors.New("web_socket_callback_on_text_message is not exported") - } - onbinarymessage := module.ExportedFunction("web_socket_callback_on_binary_message") - if onbinarymessage == nil { - return nil, errors.New("web_socket_callback_on_binary_message is not exported") - } - onerror := module.ExportedFunction("web_socket_callback_on_error") - if onerror == nil { - return nil, errors.New("web_socket_callback_on_error is not exported") - } - onclose := module.ExportedFunction("web_socket_callback_on_close") - if onclose == nil { - return nil, errors.New("web_socket_callback_on_close is not exported") - } - - malloc := module.ExportedFunction("malloc") - if malloc == nil { - return nil, errors.New("malloc is not exported") - } - - free := module.ExportedFunction("free") - if free == nil { - return nil, errors.New("free is not exported") - } - return &webSocketCallbackPlugin{ - runtime: r, - module: module, - malloc: malloc, - free: free, - ontextmessage: ontextmessage, - onbinarymessage: onbinarymessage, - onerror: onerror, - onclose: onclose, - }, nil -} - -func (p *webSocketCallbackPlugin) Close(ctx context.Context) (err error) { - if r := p.runtime; r != nil { - r.Close(ctx) - } - return -} - -type webSocketCallbackPlugin struct { - runtime wazero.Runtime - module api.Module - malloc api.Function - free api.Function - ontextmessage api.Function - onbinarymessage api.Function - onerror api.Function - onclose api.Function -} - -func (p *webSocketCallbackPlugin) OnTextMessage(ctx context.Context, request *OnTextMessageRequest) (*OnTextMessageResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.ontextmessage.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(OnTextMessageResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *webSocketCallbackPlugin) OnBinaryMessage(ctx context.Context, request *OnBinaryMessageRequest) (*OnBinaryMessageResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.onbinarymessage.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(OnBinaryMessageResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *webSocketCallbackPlugin) OnError(ctx context.Context, request *OnErrorRequest) (*OnErrorResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.onerror.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(OnErrorResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} -func (p *webSocketCallbackPlugin) OnClose(ctx context.Context, request *OnCloseRequest) (*OnCloseResponse, error) { - data, err := request.MarshalVT() - if err != nil { - return nil, err - } - dataSize := uint64(len(data)) - - var dataPtr uint64 - // If the input data is not empty, we must allocate the in-Wasm memory to store it, and pass to the plugin. - if dataSize != 0 { - results, err := p.malloc.Call(ctx, dataSize) - if err != nil { - return nil, err - } - dataPtr = results[0] - // This pointer is managed by the Wasm module, which is unaware of external usage. - // So, we have to free it when finished - defer p.free.Call(ctx, dataPtr) - - // The pointer is a linear memory offset, which is where we write the name. - if !p.module.Memory().Write(uint32(dataPtr), data) { - return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", dataPtr, dataSize, p.module.Memory().Size()) - } - } - - ptrSize, err := p.onclose.Call(ctx, dataPtr, dataSize) - if err != nil { - return nil, err - } - - resPtr := uint32(ptrSize[0] >> 32) - resSize := uint32(ptrSize[0]) - var isErrResponse bool - if (resSize & (1 << 31)) > 0 { - isErrResponse = true - resSize &^= (1 << 31) - } - - // We don't need the memory after deserialization: make sure it is freed. - if resPtr != 0 { - defer p.free.Call(ctx, uint64(resPtr)) - } - - // The pointer is a linear memory offset, which is where we write the name. - bytes, ok := p.module.Memory().Read(resPtr, resSize) - if !ok { - return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", - resPtr, resSize, p.module.Memory().Size()) - } - - if isErrResponse { - return nil, errors.New(string(bytes)) - } - - response := new(OnCloseResponse) - if err = response.UnmarshalVT(bytes); err != nil { - return nil, err - } - - return response, nil -} diff --git a/plugins/api/api_options.pb.go b/plugins/api/api_options.pb.go deleted file mode 100644 index 430bf0a5c..000000000 --- a/plugins/api/api_options.pb.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: api/api.proto - -package api - -import ( - context "context" - wazero "github.com/tetratelabs/wazero" - wasi_snapshot_preview1 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" -) - -type wazeroConfigOption func(plugin *WazeroConfig) - -type WazeroNewRuntime func(context.Context) (wazero.Runtime, error) - -type WazeroConfig struct { - newRuntime func(context.Context) (wazero.Runtime, error) - moduleConfig wazero.ModuleConfig -} - -func WazeroRuntime(newRuntime WazeroNewRuntime) wazeroConfigOption { - return func(h *WazeroConfig) { - h.newRuntime = newRuntime - } -} - -func DefaultWazeroRuntime() WazeroNewRuntime { - return func(ctx context.Context) (wazero.Runtime, error) { - r := wazero.NewRuntime(ctx) - if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil { - return nil, err - } - - return r, nil - } -} - -func WazeroModuleConfig(moduleConfig wazero.ModuleConfig) wazeroConfigOption { - return func(h *WazeroConfig) { - h.moduleConfig = moduleConfig - } -} diff --git a/plugins/api/api_plugin.pb.go b/plugins/api/api_plugin.pb.go deleted file mode 100644 index 0a022be9b..000000000 --- a/plugins/api/api_plugin.pb.go +++ /dev/null @@ -1,487 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: api/api.proto - -package api - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" -) - -const MetadataAgentPluginAPIVersion = 1 - -//go:wasmexport metadata_agent_api_version -func _metadata_agent_api_version() uint64 { - return MetadataAgentPluginAPIVersion -} - -var metadataAgent MetadataAgent - -func RegisterMetadataAgent(p MetadataAgent) { - metadataAgent = p -} - -//go:wasmexport metadata_agent_get_artist_mbid -func _metadata_agent_get_artist_mbid(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ArtistMBIDRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetArtistMBID(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_artist_url -func _metadata_agent_get_artist_url(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ArtistURLRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetArtistURL(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_artist_biography -func _metadata_agent_get_artist_biography(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ArtistBiographyRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetArtistBiography(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_similar_artists -func _metadata_agent_get_similar_artists(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ArtistSimilarRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetSimilarArtists(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_artist_images -func _metadata_agent_get_artist_images(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ArtistImageRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetArtistImages(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_artist_top_songs -func _metadata_agent_get_artist_top_songs(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ArtistTopSongsRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetArtistTopSongs(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_album_info -func _metadata_agent_get_album_info(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(AlbumInfoRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetAlbumInfo(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport metadata_agent_get_album_images -func _metadata_agent_get_album_images(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(AlbumImagesRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := metadataAgent.GetAlbumImages(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -const ScrobblerPluginAPIVersion = 1 - -//go:wasmexport scrobbler_api_version -func _scrobbler_api_version() uint64 { - return ScrobblerPluginAPIVersion -} - -var scrobbler Scrobbler - -func RegisterScrobbler(p Scrobbler) { - scrobbler = p -} - -//go:wasmexport scrobbler_is_authorized -func _scrobbler_is_authorized(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ScrobblerIsAuthorizedRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := scrobbler.IsAuthorized(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport scrobbler_now_playing -func _scrobbler_now_playing(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ScrobblerNowPlayingRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := scrobbler.NowPlaying(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport scrobbler_scrobble -func _scrobbler_scrobble(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(ScrobblerScrobbleRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := scrobbler.Scrobble(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -const SchedulerCallbackPluginAPIVersion = 1 - -//go:wasmexport scheduler_callback_api_version -func _scheduler_callback_api_version() uint64 { - return SchedulerCallbackPluginAPIVersion -} - -var schedulerCallback SchedulerCallback - -func RegisterSchedulerCallback(p SchedulerCallback) { - schedulerCallback = p -} - -//go:wasmexport scheduler_callback_on_scheduler_callback -func _scheduler_callback_on_scheduler_callback(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(SchedulerCallbackRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := schedulerCallback.OnSchedulerCallback(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -const LifecycleManagementPluginAPIVersion = 1 - -//go:wasmexport lifecycle_management_api_version -func _lifecycle_management_api_version() uint64 { - return LifecycleManagementPluginAPIVersion -} - -var lifecycleManagement LifecycleManagement - -func RegisterLifecycleManagement(p LifecycleManagement) { - lifecycleManagement = p -} - -//go:wasmexport lifecycle_management_on_init -func _lifecycle_management_on_init(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(InitRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := lifecycleManagement.OnInit(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -const WebSocketCallbackPluginAPIVersion = 1 - -//go:wasmexport web_socket_callback_api_version -func _web_socket_callback_api_version() uint64 { - return WebSocketCallbackPluginAPIVersion -} - -var webSocketCallback WebSocketCallback - -func RegisterWebSocketCallback(p WebSocketCallback) { - webSocketCallback = p -} - -//go:wasmexport web_socket_callback_on_text_message -func _web_socket_callback_on_text_message(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(OnTextMessageRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := webSocketCallback.OnTextMessage(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport web_socket_callback_on_binary_message -func _web_socket_callback_on_binary_message(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(OnBinaryMessageRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := webSocketCallback.OnBinaryMessage(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport web_socket_callback_on_error -func _web_socket_callback_on_error(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(OnErrorRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := webSocketCallback.OnError(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} - -//go:wasmexport web_socket_callback_on_close -func _web_socket_callback_on_close(ptr, size uint32) uint64 { - b := wasm.PtrToByte(ptr, size) - req := new(OnCloseRequest) - if err := req.UnmarshalVT(b); err != nil { - return 0 - } - response, err := webSocketCallback.OnClose(context.Background(), req) - if err != nil { - ptr, size = wasm.ByteToPtr([]byte(err.Error())) - return (uint64(ptr) << uint64(32)) | uint64(size) | - // Indicate that this is the error string by setting the 32-th bit, assuming that - // no data exceeds 31-bit size (2 GiB). - (1 << 31) - } - - b, err = response.MarshalVT() - if err != nil { - return 0 - } - ptr, size = wasm.ByteToPtr(b) - return (uint64(ptr) << uint64(32)) | uint64(size) -} diff --git a/plugins/api/api_plugin_dev.go b/plugins/api/api_plugin_dev.go deleted file mode 100644 index ed5a064b2..000000000 --- a/plugins/api/api_plugin_dev.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build !wasip1 - -package api - -import "github.com/navidrome/navidrome/plugins/host/scheduler" - -// This file exists to provide stubs for the plugin registration functions when building for non-WASM targets. -// This is useful for testing and development purposes, as it allows you to build and run your plugin code -// without having to compile it to WASM. -// In a real-world scenario, you would compile your plugin to WASM and use the generated registration functions. - -func RegisterMetadataAgent(MetadataAgent) { - panic("not implemented") -} - -func RegisterScrobbler(Scrobbler) { - panic("not implemented") -} - -func RegisterSchedulerCallback(SchedulerCallback) { - panic("not implemented") -} - -func RegisterLifecycleManagement(LifecycleManagement) { - panic("not implemented") -} - -func RegisterWebSocketCallback(WebSocketCallback) { - panic("not implemented") -} - -func RegisterNamedSchedulerCallback(name string, cb SchedulerCallback) scheduler.SchedulerService { - panic("not implemented") -} diff --git a/plugins/api/api_plugin_dev_named_registry.go b/plugins/api/api_plugin_dev_named_registry.go deleted file mode 100644 index 2ddb68779..000000000 --- a/plugins/api/api_plugin_dev_named_registry.go +++ /dev/null @@ -1,94 +0,0 @@ -//go:build wasip1 - -package api - -import ( - "context" - "strings" - - "github.com/navidrome/navidrome/plugins/host/scheduler" -) - -var callbacks = make(namedCallbacks) - -// RegisterNamedSchedulerCallback registers a named scheduler callback. Named callbacks allow multiple callbacks to be registered -// within the same plugin, and for the schedules to be scoped to the named callback. If you only need a single callback, you can use -// the default (unnamed) callback registration function, RegisterSchedulerCallback. -// It returns a scheduler.SchedulerService that can be used to schedule jobs for the named callback. -// -// Notes: -// -// - You can't mix named and unnamed callbacks within the same plugin. -// - The name should be unique within the plugin, and it's recommended to use a short, descriptive name. -// - The name is case-sensitive. -func RegisterNamedSchedulerCallback(name string, cb SchedulerCallback) scheduler.SchedulerService { - callbacks[name] = cb - RegisterSchedulerCallback(&callbacks) - return &namedSchedulerService{name: name, svc: scheduler.NewSchedulerService()} -} - -const zwsp = string('\u200b') - -// namedCallbacks is a map of named scheduler callbacks. The key is the name of the callback, and the value is the callback itself. -type namedCallbacks map[string]SchedulerCallback - -func parseKey(key string) (string, string) { - parts := strings.SplitN(key, zwsp, 2) - if len(parts) != 2 { - return "", "" - } - return parts[0], parts[1] -} - -func (n *namedCallbacks) OnSchedulerCallback(ctx context.Context, req *SchedulerCallbackRequest) (*SchedulerCallbackResponse, error) { - name, scheduleId := parseKey(req.ScheduleId) - cb, exists := callbacks[name] - if !exists { - return nil, nil - } - req.ScheduleId = scheduleId - return cb.OnSchedulerCallback(ctx, req) -} - -// namedSchedulerService is a wrapper around the host scheduler service that prefixes the schedule IDs with the -// callback name. It is returned by RegisterNamedSchedulerCallback, and should be used by the plugin to schedule -// jobs for the named callback. -type namedSchedulerService struct { - name string - cb SchedulerCallback - svc scheduler.SchedulerService -} - -func (n *namedSchedulerService) makeKey(id string) string { - return n.name + zwsp + id -} - -func (n *namedSchedulerService) mapResponse(resp *scheduler.ScheduleResponse, err error) (*scheduler.ScheduleResponse, error) { - if err != nil { - return nil, err - } - _, resp.ScheduleId = parseKey(resp.ScheduleId) - return resp, nil -} - -func (n *namedSchedulerService) ScheduleOneTime(ctx context.Context, request *scheduler.ScheduleOneTimeRequest) (*scheduler.ScheduleResponse, error) { - key := n.makeKey(request.ScheduleId) - request.ScheduleId = key - return n.mapResponse(n.svc.ScheduleOneTime(ctx, request)) -} - -func (n *namedSchedulerService) ScheduleRecurring(ctx context.Context, request *scheduler.ScheduleRecurringRequest) (*scheduler.ScheduleResponse, error) { - key := n.makeKey(request.ScheduleId) - request.ScheduleId = key - return n.mapResponse(n.svc.ScheduleRecurring(ctx, request)) -} - -func (n *namedSchedulerService) CancelSchedule(ctx context.Context, request *scheduler.CancelRequest) (*scheduler.CancelResponse, error) { - key := n.makeKey(request.ScheduleId) - request.ScheduleId = key - return n.svc.CancelSchedule(ctx, request) -} - -func (n *namedSchedulerService) TimeNow(ctx context.Context, request *scheduler.TimeNowRequest) (*scheduler.TimeNowResponse, error) { - return n.svc.TimeNow(ctx, request) -} diff --git a/plugins/api/api_vtproto.pb.go b/plugins/api/api_vtproto.pb.go deleted file mode 100644 index 11caa1946..000000000 --- a/plugins/api/api_vtproto.pb.go +++ /dev/null @@ -1,7315 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: api/api.proto - -package api - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *ArtistMBIDRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistMBIDRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistMBIDRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistMBIDResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistMBIDResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistMBIDResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistURLRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistURLRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistURLRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistURLResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistURLResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistURLResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistBiographyRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistBiographyRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistBiographyRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistBiographyResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistBiographyResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistBiographyResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Biography) > 0 { - i -= len(m.Biography) - copy(dAtA[i:], m.Biography) - i = encodeVarint(dAtA, i, uint64(len(m.Biography))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistSimilarRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistSimilarRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistSimilarRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Limit != 0 { - i = encodeVarint(dAtA, i, uint64(m.Limit)) - i-- - dAtA[i] = 0x20 - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *Artist) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *Artist) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *Artist) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistSimilarResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistSimilarResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistSimilarResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Artists) > 0 { - for iNdEx := len(m.Artists) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.Artists[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *ArtistImageRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistImageRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistImageRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ExternalImage) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ExternalImage) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ExternalImage) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Size != 0 { - i = encodeVarint(dAtA, i, uint64(m.Size)) - i-- - dAtA[i] = 0x10 - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistImageResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistImageResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistImageResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Images) > 0 { - for iNdEx := len(m.Images) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.Images[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *ArtistTopSongsRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistTopSongsRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistTopSongsRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Count != 0 { - i = encodeVarint(dAtA, i, uint64(m.Count)) - i-- - dAtA[i] = 0x20 - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.ArtistName) > 0 { - i -= len(m.ArtistName) - copy(dAtA[i:], m.ArtistName) - i = encodeVarint(dAtA, i, uint64(len(m.ArtistName))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *Song) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *Song) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *Song) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ArtistTopSongsResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ArtistTopSongsResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ArtistTopSongsResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Songs) > 0 { - for iNdEx := len(m.Songs) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.Songs[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *AlbumInfoRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AlbumInfoRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *AlbumInfoRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.Artist) > 0 { - i -= len(m.Artist) - copy(dAtA[i:], m.Artist) - i = encodeVarint(dAtA, i, uint64(len(m.Artist))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *AlbumInfo) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AlbumInfo) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *AlbumInfo) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0x22 - } - if len(m.Description) > 0 { - i -= len(m.Description) - copy(dAtA[i:], m.Description) - i = encodeVarint(dAtA, i, uint64(len(m.Description))) - i-- - dAtA[i] = 0x1a - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *AlbumInfoResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AlbumInfoResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *AlbumInfoResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Info != nil { - size, err := m.Info.MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *AlbumImagesRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AlbumImagesRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *AlbumImagesRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x1a - } - if len(m.Artist) > 0 { - i -= len(m.Artist) - copy(dAtA[i:], m.Artist) - i = encodeVarint(dAtA, i, uint64(len(m.Artist))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *AlbumImagesResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AlbumImagesResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *AlbumImagesResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Images) > 0 { - for iNdEx := len(m.Images) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.Images[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *ScrobblerIsAuthorizedRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScrobblerIsAuthorizedRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScrobblerIsAuthorizedRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Username) > 0 { - i -= len(m.Username) - copy(dAtA[i:], m.Username) - i = encodeVarint(dAtA, i, uint64(len(m.Username))) - i-- - dAtA[i] = 0x12 - } - if len(m.UserId) > 0 { - i -= len(m.UserId) - copy(dAtA[i:], m.UserId) - i = encodeVarint(dAtA, i, uint64(len(m.UserId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ScrobblerIsAuthorizedResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScrobblerIsAuthorizedResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScrobblerIsAuthorizedResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0x12 - } - if m.Authorized { - i-- - if m.Authorized { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *TrackInfo) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *TrackInfo) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *TrackInfo) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Position != 0 { - i = encodeVarint(dAtA, i, uint64(m.Position)) - i-- - dAtA[i] = 0x48 - } - if m.Length != 0 { - i = encodeVarint(dAtA, i, uint64(m.Length)) - i-- - dAtA[i] = 0x40 - } - if len(m.AlbumArtists) > 0 { - for iNdEx := len(m.AlbumArtists) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.AlbumArtists[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x3a - } - } - if len(m.Artists) > 0 { - for iNdEx := len(m.Artists) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.Artists[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x32 - } - } - if len(m.AlbumMbid) > 0 { - i -= len(m.AlbumMbid) - copy(dAtA[i:], m.AlbumMbid) - i = encodeVarint(dAtA, i, uint64(len(m.AlbumMbid))) - i-- - dAtA[i] = 0x2a - } - if len(m.Album) > 0 { - i -= len(m.Album) - copy(dAtA[i:], m.Album) - i = encodeVarint(dAtA, i, uint64(len(m.Album))) - i-- - dAtA[i] = 0x22 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarint(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x1a - } - if len(m.Mbid) > 0 { - i -= len(m.Mbid) - copy(dAtA[i:], m.Mbid) - i = encodeVarint(dAtA, i, uint64(len(m.Mbid))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ScrobblerNowPlayingRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScrobblerNowPlayingRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScrobblerNowPlayingRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Timestamp != 0 { - i = encodeVarint(dAtA, i, uint64(m.Timestamp)) - i-- - dAtA[i] = 0x20 - } - if m.Track != nil { - size, err := m.Track.MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x1a - } - if len(m.Username) > 0 { - i -= len(m.Username) - copy(dAtA[i:], m.Username) - i = encodeVarint(dAtA, i, uint64(len(m.Username))) - i-- - dAtA[i] = 0x12 - } - if len(m.UserId) > 0 { - i -= len(m.UserId) - copy(dAtA[i:], m.UserId) - i = encodeVarint(dAtA, i, uint64(len(m.UserId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ScrobblerNowPlayingResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScrobblerNowPlayingResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScrobblerNowPlayingResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ScrobblerScrobbleRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScrobblerScrobbleRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScrobblerScrobbleRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Timestamp != 0 { - i = encodeVarint(dAtA, i, uint64(m.Timestamp)) - i-- - dAtA[i] = 0x20 - } - if m.Track != nil { - size, err := m.Track.MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x1a - } - if len(m.Username) > 0 { - i -= len(m.Username) - copy(dAtA[i:], m.Username) - i = encodeVarint(dAtA, i, uint64(len(m.Username))) - i-- - dAtA[i] = 0x12 - } - if len(m.UserId) > 0 { - i -= len(m.UserId) - copy(dAtA[i:], m.UserId) - i = encodeVarint(dAtA, i, uint64(len(m.UserId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ScrobblerScrobbleResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScrobblerScrobbleResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScrobblerScrobbleResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SchedulerCallbackRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SchedulerCallbackRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SchedulerCallbackRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.IsRecurring { - i-- - if m.IsRecurring { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x18 - } - if len(m.Payload) > 0 { - i -= len(m.Payload) - copy(dAtA[i:], m.Payload) - i = encodeVarint(dAtA, i, uint64(len(m.Payload))) - i-- - dAtA[i] = 0x12 - } - if len(m.ScheduleId) > 0 { - i -= len(m.ScheduleId) - copy(dAtA[i:], m.ScheduleId) - i = encodeVarint(dAtA, i, uint64(len(m.ScheduleId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SchedulerCallbackResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SchedulerCallbackResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SchedulerCallbackResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *InitRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *InitRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *InitRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Config) > 0 { - for k := range m.Config { - v := m.Config[k] - baseI := i - i -= len(v) - copy(dAtA[i:], v) - i = encodeVarint(dAtA, i, uint64(len(v))) - i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = encodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = encodeVarint(dAtA, i, uint64(baseI-i)) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *InitResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *InitResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *InitResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *OnTextMessageRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnTextMessageRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnTextMessageRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Message) > 0 { - i -= len(m.Message) - copy(dAtA[i:], m.Message) - i = encodeVarint(dAtA, i, uint64(len(m.Message))) - i-- - dAtA[i] = 0x12 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *OnTextMessageResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnTextMessageResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnTextMessageResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - return len(dAtA) - i, nil -} - -func (m *OnBinaryMessageRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnBinaryMessageRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnBinaryMessageRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Data) > 0 { - i -= len(m.Data) - copy(dAtA[i:], m.Data) - i = encodeVarint(dAtA, i, uint64(len(m.Data))) - i-- - dAtA[i] = 0x12 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *OnBinaryMessageResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnBinaryMessageResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnBinaryMessageResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - return len(dAtA) - i, nil -} - -func (m *OnErrorRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnErrorRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnErrorRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0x12 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *OnErrorResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnErrorResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnErrorResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - return len(dAtA) - i, nil -} - -func (m *OnCloseRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnCloseRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnCloseRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Reason) > 0 { - i -= len(m.Reason) - copy(dAtA[i:], m.Reason) - i = encodeVarint(dAtA, i, uint64(len(m.Reason))) - i-- - dAtA[i] = 0x1a - } - if m.Code != 0 { - i = encodeVarint(dAtA, i, uint64(m.Code)) - i-- - dAtA[i] = 0x10 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *OnCloseResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *OnCloseResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *OnCloseResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *ArtistMBIDRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistMBIDResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistURLRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistURLResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistBiographyRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistBiographyResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Biography) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistSimilarRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Limit != 0 { - n += 1 + sov(uint64(m.Limit)) - } - n += len(m.unknownFields) - return n -} - -func (m *Artist) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistSimilarResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Artists) > 0 { - for _, e := range m.Artists { - l = e.SizeVT() - n += 1 + l + sov(uint64(l)) - } - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistImageRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ExternalImage) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Size != 0 { - n += 1 + sov(uint64(m.Size)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistImageResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Images) > 0 { - for _, e := range m.Images { - l = e.SizeVT() - n += 1 + l + sov(uint64(l)) - } - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistTopSongsRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.ArtistName) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Count != 0 { - n += 1 + sov(uint64(m.Count)) - } - n += len(m.unknownFields) - return n -} - -func (m *Song) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ArtistTopSongsResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Songs) > 0 { - for _, e := range m.Songs { - l = e.SizeVT() - n += 1 + l + sov(uint64(l)) - } - } - n += len(m.unknownFields) - return n -} - -func (m *AlbumInfoRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Artist) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *AlbumInfo) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Description) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *AlbumInfoResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Info != nil { - l = m.Info.SizeVT() - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *AlbumImagesRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Artist) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *AlbumImagesResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Images) > 0 { - for _, e := range m.Images { - l = e.SizeVT() - n += 1 + l + sov(uint64(l)) - } - } - n += len(m.unknownFields) - return n -} - -func (m *ScrobblerIsAuthorizedRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.UserId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Username) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScrobblerIsAuthorizedResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Authorized { - n += 2 - } - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *TrackInfo) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Mbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Album) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.AlbumMbid) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if len(m.Artists) > 0 { - for _, e := range m.Artists { - l = e.SizeVT() - n += 1 + l + sov(uint64(l)) - } - } - if len(m.AlbumArtists) > 0 { - for _, e := range m.AlbumArtists { - l = e.SizeVT() - n += 1 + l + sov(uint64(l)) - } - } - if m.Length != 0 { - n += 1 + sov(uint64(m.Length)) - } - if m.Position != 0 { - n += 1 + sov(uint64(m.Position)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScrobblerNowPlayingRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.UserId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Username) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Track != nil { - l = m.Track.SizeVT() - n += 1 + l + sov(uint64(l)) - } - if m.Timestamp != 0 { - n += 1 + sov(uint64(m.Timestamp)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScrobblerNowPlayingResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScrobblerScrobbleRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.UserId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Username) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Track != nil { - l = m.Track.SizeVT() - n += 1 + l + sov(uint64(l)) - } - if m.Timestamp != 0 { - n += 1 + sov(uint64(m.Timestamp)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScrobblerScrobbleResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *SchedulerCallbackRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ScheduleId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Payload) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.IsRecurring { - n += 2 - } - n += len(m.unknownFields) - return n -} - -func (m *SchedulerCallbackResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *InitRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Config) > 0 { - for k, v := range m.Config { - _ = k - _ = v - mapEntrySize := 1 + len(k) + sov(uint64(len(k))) + 1 + len(v) + sov(uint64(len(v))) - n += mapEntrySize + 1 + sov(uint64(mapEntrySize)) - } - } - n += len(m.unknownFields) - return n -} - -func (m *InitResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *OnTextMessageRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Message) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *OnTextMessageResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - n += len(m.unknownFields) - return n -} - -func (m *OnBinaryMessageRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Data) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *OnBinaryMessageResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - n += len(m.unknownFields) - return n -} - -func (m *OnErrorRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *OnErrorResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - n += len(m.unknownFields) - return n -} - -func (m *OnCloseRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Code != 0 { - n += 1 + sov(uint64(m.Code)) - } - l = len(m.Reason) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *OnCloseResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *ArtistMBIDRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistMBIDRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistMBIDRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistMBIDResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistMBIDResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistMBIDResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistURLRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistURLRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistURLRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistURLResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistURLResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistURLResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistBiographyRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistBiographyRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistBiographyRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistBiographyResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistBiographyResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistBiographyResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Biography", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Biography = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistSimilarRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistSimilarRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistSimilarRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) - } - m.Limit = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Limit |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *Artist) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Artist: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Artist: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistSimilarResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistSimilarResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistSimilarResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Artists", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Artists = append(m.Artists, &Artist{}) - if err := m.Artists[len(m.Artists)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistImageRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistImageRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistImageRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ExternalImage) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ExternalImage: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ExternalImage: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Size", wireType) - } - m.Size = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Size |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistImageResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistImageResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistImageResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Images", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Images = append(m.Images, &ExternalImage{}) - if err := m.Images[len(m.Images)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistTopSongsRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistTopSongsRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistTopSongsRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ArtistName", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ArtistName = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Count", wireType) - } - m.Count = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Count |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *Song) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Song: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Song: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ArtistTopSongsResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ArtistTopSongsResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ArtistTopSongsResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Songs", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Songs = append(m.Songs, &Song{}) - if err := m.Songs[len(m.Songs)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AlbumInfoRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AlbumInfoRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AlbumInfoRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Artist", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Artist = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AlbumInfo) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AlbumInfo: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AlbumInfo: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Description = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AlbumInfoResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AlbumInfoResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AlbumInfoResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Info", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Info == nil { - m.Info = &AlbumInfo{} - } - if err := m.Info.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AlbumImagesRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AlbumImagesRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AlbumImagesRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Artist", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Artist = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AlbumImagesResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AlbumImagesResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AlbumImagesResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Images", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Images = append(m.Images, &ExternalImage{}) - if err := m.Images[len(m.Images)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScrobblerIsAuthorizedRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScrobblerIsAuthorizedRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScrobblerIsAuthorizedRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field UserId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.UserId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Username = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScrobblerIsAuthorizedResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScrobblerIsAuthorizedResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScrobblerIsAuthorizedResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Authorized", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Authorized = bool(v != 0) - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *TrackInfo) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: TrackInfo: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: TrackInfo: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Mbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Mbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Album", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Album = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field AlbumMbid", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.AlbumMbid = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 6: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Artists", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Artists = append(m.Artists, &Artist{}) - if err := m.Artists[len(m.Artists)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 7: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field AlbumArtists", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.AlbumArtists = append(m.AlbumArtists, &Artist{}) - if err := m.AlbumArtists[len(m.AlbumArtists)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 8: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Length", wireType) - } - m.Length = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Length |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 9: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Position", wireType) - } - m.Position = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Position |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScrobblerNowPlayingRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScrobblerNowPlayingRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScrobblerNowPlayingRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field UserId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.UserId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Username = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Track", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Track == nil { - m.Track = &TrackInfo{} - } - if err := m.Track.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) - } - m.Timestamp = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Timestamp |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScrobblerNowPlayingResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScrobblerNowPlayingResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScrobblerNowPlayingResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScrobblerScrobbleRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScrobblerScrobbleRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScrobblerScrobbleRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field UserId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.UserId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Username = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Track", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Track == nil { - m.Track = &TrackInfo{} - } - if err := m.Track.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) - } - m.Timestamp = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Timestamp |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScrobblerScrobbleResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScrobblerScrobbleResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScrobblerScrobbleResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SchedulerCallbackRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SchedulerCallbackRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SchedulerCallbackRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScheduleId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ScheduleId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Payload", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Payload = append(m.Payload[:0], dAtA[iNdEx:postIndex]...) - if m.Payload == nil { - m.Payload = []byte{} - } - iNdEx = postIndex - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field IsRecurring", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.IsRecurring = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SchedulerCallbackResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SchedulerCallbackResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SchedulerCallbackResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *InitRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: InitRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: InitRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Config", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Config == nil { - m.Config = make(map[string]string) - } - var mapkey string - var mapvalue string - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var stringLenmapvalue uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapvalue |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapvalue := int(stringLenmapvalue) - if intStringLenmapvalue < 0 { - return ErrInvalidLength - } - postStringIndexmapvalue := iNdEx + intStringLenmapvalue - if postStringIndexmapvalue < 0 { - return ErrInvalidLength - } - if postStringIndexmapvalue > l { - return io.ErrUnexpectedEOF - } - mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) - iNdEx = postStringIndexmapvalue - } else { - iNdEx = entryPreIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Config[mapkey] = mapvalue - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *InitResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: InitResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: InitResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnTextMessageRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnTextMessageRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnTextMessageRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Message = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnTextMessageResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnTextMessageResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnTextMessageResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnBinaryMessageRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnBinaryMessageRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnBinaryMessageRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) - if m.Data == nil { - m.Data = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnBinaryMessageResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnBinaryMessageResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnBinaryMessageResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnErrorRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnErrorRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnErrorRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnErrorResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnErrorResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnErrorResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnCloseRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnCloseRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnCloseRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Code", wireType) - } - m.Code = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Code |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Reason", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Reason = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *OnCloseResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: OnCloseResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: OnCloseResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/api/errors.go b/plugins/api/errors.go deleted file mode 100644 index 796774b15..000000000 --- a/plugins/api/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package api - -import "errors" - -var ( - // ErrNotImplemented indicates that the plugin does not implement the requested method. - // No logic should be executed by the plugin. - ErrNotImplemented = errors.New("plugin:not_implemented") - - // ErrNotFound indicates that the requested resource was not found by the plugin. - ErrNotFound = errors.New("plugin:not_found") -) diff --git a/plugins/base_capability.go b/plugins/base_capability.go deleted file mode 100644 index 6572a25ec..000000000 --- a/plugins/base_capability.go +++ /dev/null @@ -1,159 +0,0 @@ -package plugins - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model/id" - "github.com/navidrome/navidrome/plugins/api" -) - -// newBaseCapability creates a new instance of baseCapability with the required parameters. -func newBaseCapability[S any, P any](wasmPath, id, capability string, m metrics.Metrics, loader P, loadFunc loaderFunc[S, P]) *baseCapability[S, P] { - return &baseCapability[S, P]{ - wasmPath: wasmPath, - id: id, - capability: capability, - loader: loader, - loadFunc: loadFunc, - metrics: m, - } -} - -// LoaderFunc is a generic function type that loads a plugin instance. -type loaderFunc[S any, P any] func(ctx context.Context, loader P, path string) (S, error) - -// baseCapability is a generic base implementation for WASM plugins. -// S is the capability interface type and P is the plugin loader type. -type baseCapability[S any, P any] struct { - wasmPath string - id string - capability string - loader P - loadFunc loaderFunc[S, P] - metrics metrics.Metrics -} - -func (w *baseCapability[S, P]) PluginID() string { - return w.id -} - -func (w *baseCapability[S, P]) serviceName() string { - return w.id + "_" + w.capability -} - -func (w *baseCapability[S, P]) getMetrics() metrics.Metrics { - return w.metrics -} - -// getInstance loads a new plugin instance and returns a cleanup function. -func (w *baseCapability[S, P]) getInstance(ctx context.Context, methodName string) (S, func(), error) { - start := time.Now() - // Add context metadata for tracing - ctx = log.NewContext(ctx, "capability", w.serviceName(), "method", methodName) - - inst, err := w.loadFunc(ctx, w.loader, w.wasmPath) - if err != nil { - var zero S - return zero, func() {}, fmt.Errorf("baseCapability: failed to load instance for %s: %w", w.serviceName(), err) - } - // Add context metadata for tracing - ctx = log.NewContext(ctx, "instanceID", getInstanceID(inst)) - log.Trace(ctx, "baseCapability: loaded instance", "elapsed", time.Since(start)) - return inst, func() { - log.Trace(ctx, "baseCapability: finished using instance", "elapsed", time.Since(start)) - if closer, ok := any(inst).(interface{ Close(context.Context) error }); ok { - _ = closer.Close(ctx) - } - }, nil -} - -type wasmPlugin[S any] interface { - PluginID() string - getInstance(ctx context.Context, methodName string) (S, func(), error) - getMetrics() metrics.Metrics -} - -func callMethod[S any, R any](ctx context.Context, wp WasmPlugin, methodName string, fn func(inst S) (R, error)) (R, error) { - // Add a unique call ID to the context for tracing - ctx = log.NewContext(ctx, "callID", id.NewRandom()) - var r R - - p, ok := wp.(wasmPlugin[S]) - if !ok { - log.Error(ctx, "callMethod: not a wasm plugin", "method", methodName, "pluginID", wp.PluginID()) - return r, fmt.Errorf("wasm plugin: not a wasm plugin: %s", wp.PluginID()) - } - - inst, done, err := p.getInstance(ctx, methodName) - if err != nil { - return r, err - } - start := time.Now() - defer done() - r, err = checkErr(fn(inst)) - elapsed := time.Since(start) - - if !errors.Is(err, api.ErrNotImplemented) { - id := p.PluginID() - isOk := err == nil - metrics := p.getMetrics() - if metrics != nil { - metrics.RecordPluginRequest(ctx, id, methodName, isOk, elapsed.Milliseconds()) - log.Trace(ctx, "callMethod: sending metrics", "plugin", id, "method", methodName, "ok", isOk, "elapsed", elapsed) - } - } - - return r, err -} - -// errorResponse is an interface that defines a method to retrieve an error message. -// It is automatically implemented (generated) by all plugin responses that have an Error field -type errorResponse interface { - GetError() string -} - -// checkErr returns an updated error if the response implements errorResponse and contains an error message. -// If the response is nil, it returns the original error. Otherwise, it wraps or creates an error as needed. -// It also maps error strings to their corresponding api.Err* constants. -func checkErr[T any](resp T, err error) (T, error) { - if any(resp) == nil { - return resp, mapAPIError(err) - } - respErr, ok := any(resp).(errorResponse) - if ok && respErr.GetError() != "" { - respErrMsg := respErr.GetError() - respErrErr := errors.New(respErrMsg) - mappedErr := mapAPIError(respErrErr) - // Check if the error was mapped to an API error (different from the temp error) - if errors.Is(mappedErr, api.ErrNotImplemented) || errors.Is(mappedErr, api.ErrNotFound) { - // Return the mapped API error instead of wrapping - return resp, mappedErr - } - // For non-API errors, use wrap the original error if it is not nil - return resp, errors.Join(respErrErr, err) - } - return resp, mapAPIError(err) -} - -// mapAPIError maps error strings to their corresponding api.Err* constants. -// This is needed as errors from plugins may not be of type api.Error, due to serialization/deserialization. -func mapAPIError(err error) error { - if err == nil { - return nil - } - - errStr := err.Error() - switch errStr { - case api.ErrNotImplemented.Error(): - return api.ErrNotImplemented - case api.ErrNotFound.Error(): - return api.ErrNotFound - default: - return err - } -} diff --git a/plugins/base_capability_test.go b/plugins/base_capability_test.go deleted file mode 100644 index 3bece8dcd..000000000 --- a/plugins/base_capability_test.go +++ /dev/null @@ -1,285 +0,0 @@ -package plugins - -import ( - "context" - "errors" - - "github.com/navidrome/navidrome/plugins/api" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -type nilInstance struct{} - -var _ = Describe("baseCapability", func() { - var ctx = context.Background() - - It("should load instance using loadFunc", func() { - called := false - plugin := &baseCapability[*nilInstance, any]{ - wasmPath: "", - id: "test", - capability: "test", - loadFunc: func(ctx context.Context, _ any, path string) (*nilInstance, error) { - called = true - return &nilInstance{}, nil - }, - } - inst, done, err := plugin.getInstance(ctx, "test") - defer done() - Expect(err).To(BeNil()) - Expect(inst).ToNot(BeNil()) - Expect(called).To(BeTrue()) - }) -}) - -var _ = Describe("checkErr", func() { - Context("when resp is nil", func() { - It("should return nil error when both resp and err are nil", func() { - var resp *testErrorResponse - - result, err := checkErr(resp, nil) - - Expect(result).To(BeNil()) - Expect(err).To(BeNil()) - }) - - It("should return original error unchanged for non-API errors", func() { - var resp *testErrorResponse - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(BeNil()) - Expect(err).To(Equal(originalErr)) - }) - - It("should return mapped API error for ErrNotImplemented", func() { - var resp *testErrorResponse - err := errors.New("plugin:not_implemented") - - result, mappedErr := checkErr(resp, err) - - Expect(result).To(BeNil()) - Expect(mappedErr).To(Equal(api.ErrNotImplemented)) - }) - - It("should return mapped API error for ErrNotFound", func() { - var resp *testErrorResponse - err := errors.New("plugin:not_found") - - result, mappedErr := checkErr(resp, err) - - Expect(result).To(BeNil()) - Expect(mappedErr).To(Equal(api.ErrNotFound)) - }) - }) - - Context("when resp is a typed nil that implements errorResponse", func() { - It("should not panic and return original error", func() { - var resp *testErrorResponse // typed nil - originalErr := errors.New("original error") - - // This should not panic - result, err := checkErr(resp, originalErr) - - Expect(result).To(BeNil()) - Expect(err).To(Equal(originalErr)) - }) - - It("should handle typed nil with nil error gracefully", func() { - var resp *testErrorResponse // typed nil - - // This should not panic - result, err := checkErr(resp, nil) - - Expect(result).To(BeNil()) - Expect(err).To(BeNil()) - }) - }) - - Context("when resp implements errorResponse with non-empty error", func() { - It("should create new error when original error is nil", func() { - resp := &testErrorResponse{errorMsg: "plugin error"} - - result, err := checkErr(resp, nil) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError("plugin error")) - }) - - It("should wrap original error when both exist", func() { - resp := &testErrorResponse{errorMsg: "plugin error"} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(HaveOccurred()) - // Check that both error messages are present in the joined error - errStr := err.Error() - Expect(errStr).To(ContainSubstring("plugin error")) - Expect(errStr).To(ContainSubstring("original error")) - }) - - It("should return mapped API error for ErrNotImplemented when no original error", func() { - resp := &testErrorResponse{errorMsg: "plugin:not_implemented"} - - result, err := checkErr(resp, nil) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotImplemented)) - }) - - It("should return mapped API error for ErrNotFound when no original error", func() { - resp := &testErrorResponse{errorMsg: "plugin:not_found"} - - result, err := checkErr(resp, nil) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotFound)) - }) - - It("should return mapped API error for ErrNotImplemented even with original error", func() { - resp := &testErrorResponse{errorMsg: "plugin:not_implemented"} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotImplemented)) - }) - - It("should return mapped API error for ErrNotFound even with original error", func() { - resp := &testErrorResponse{errorMsg: "plugin:not_found"} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotFound)) - }) - }) - - Context("when resp implements errorResponse with empty error", func() { - It("should return original error unchanged", func() { - resp := &testErrorResponse{errorMsg: ""} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(originalErr)) - }) - - It("should return nil error when both are empty/nil", func() { - resp := &testErrorResponse{errorMsg: ""} - - result, err := checkErr(resp, nil) - - Expect(result).To(Equal(resp)) - Expect(err).To(BeNil()) - }) - - It("should map original API error when response error is empty", func() { - resp := &testErrorResponse{errorMsg: ""} - originalErr := errors.New("plugin:not_implemented") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotImplemented)) - }) - }) - - Context("when resp does not implement errorResponse", func() { - It("should return original error unchanged", func() { - resp := &testNonErrorResponse{data: "some data"} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(Equal(originalErr)) - }) - - It("should return nil error when original error is nil", func() { - resp := &testNonErrorResponse{data: "some data"} - - result, err := checkErr(resp, nil) - - Expect(result).To(Equal(resp)) - Expect(err).To(BeNil()) - }) - - It("should map original API error when response doesn't implement errorResponse", func() { - resp := &testNonErrorResponse{data: "some data"} - originalErr := errors.New("plugin:not_found") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotFound)) - }) - }) - - Context("when resp is a value type (not pointer)", func() { - It("should handle value types that implement errorResponse", func() { - resp := testValueErrorResponse{errorMsg: "value error"} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(HaveOccurred()) - // Check that both error messages are present in the joined error - errStr := err.Error() - Expect(errStr).To(ContainSubstring("value error")) - Expect(errStr).To(ContainSubstring("original error")) - }) - - It("should handle value types with empty error", func() { - resp := testValueErrorResponse{errorMsg: ""} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(originalErr)) - }) - - It("should handle value types with API error", func() { - resp := testValueErrorResponse{errorMsg: "plugin:not_implemented"} - originalErr := errors.New("original error") - - result, err := checkErr(resp, originalErr) - - Expect(result).To(Equal(resp)) - Expect(err).To(MatchError(api.ErrNotImplemented)) - }) - }) -}) - -// Test helper types -type testErrorResponse struct { - errorMsg string -} - -func (t *testErrorResponse) GetError() string { - if t == nil { - return "" // This is what would typically happen with a typed nil - } - return t.errorMsg -} - -type testNonErrorResponse struct { - data string -} - -type testValueErrorResponse struct { - errorMsg string -} - -func (t testValueErrorResponse) GetError() string { - return t.errorMsg -} diff --git a/plugins/discovery.go b/plugins/discovery.go deleted file mode 100644 index 4125da322..000000000 --- a/plugins/discovery.go +++ /dev/null @@ -1,145 +0,0 @@ -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 -} diff --git a/plugins/discovery_test.go b/plugins/discovery_test.go deleted file mode 100644 index a5fd34516..000000000 --- a/plugins/discovery_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package plugins - -import ( - "os" - "path/filepath" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("DiscoverPlugins", func() { - var tempPluginsDir string - - // Helper to create a valid plugin for discovery testing - createValidPlugin := func(name, manifestName, author, version string, capabilities []string) { - pluginDir := filepath.Join(tempPluginsDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Copy real WASM file from testdata - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(pluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - - manifest := `{ - "name": "` + manifestName + `", - "version": "` + version + `", - "capabilities": [` - for i, cap := range capabilities { - if i > 0 { - manifest += `, ` - } - manifest += `"` + cap + `"` - } - manifest += `], - "author": "` + author + `", - "description": "Test Plugin", - "website": "https://test.navidrome.org/` + manifestName + `", - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - } - - createManifestOnlyPlugin := func(name string) { - pluginDir := filepath.Join(tempPluginsDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - manifest := `{ - "name": "manifest-only", - "version": "1.0.0", - "capabilities": ["MetadataAgent"], - "author": "Test Author", - "description": "Test Plugin", - "website": "https://test.navidrome.org/manifest-only", - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - } - - createWasmOnlyPlugin := func(name string) { - pluginDir := filepath.Join(tempPluginsDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Copy real WASM file from testdata - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(pluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - } - - createInvalidManifestPlugin := func(name string) { - pluginDir := filepath.Join(tempPluginsDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Copy real WASM file from testdata - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(pluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - - invalidManifest := `{ "invalid": "json" }` - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(invalidManifest), 0600)).To(Succeed()) - } - - createEmptyCapabilitiesPlugin := func(name string) { - pluginDir := filepath.Join(tempPluginsDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Copy real WASM file from testdata - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(pluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - - manifest := `{ - "name": "empty-capabilities", - "version": "1.0.0", - "capabilities": [], - "author": "Test Author", - "description": "Test Plugin", - "website": "https://test.navidrome.org/empty-capabilities", - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - } - - BeforeEach(func() { - tempPluginsDir, _ = os.MkdirTemp("", "navidrome-plugins-discovery-test-*") - DeferCleanup(func() { - _ = os.RemoveAll(tempPluginsDir) - }) - }) - - Context("Valid plugins", func() { - It("should discover valid plugins with all required files", func() { - createValidPlugin("test-plugin", "Test Plugin", "Test Author", "1.0.0", []string{"MetadataAgent"}) - createValidPlugin("another-plugin", "Another Plugin", "Another Author", "2.0.0", []string{"Scrobbler"}) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(2)) - - // Find each plugin by ID - var testPlugin, anotherPlugin *PluginDiscoveryEntry - for i := range discoveries { - switch discoveries[i].ID { - case "test-plugin": - testPlugin = &discoveries[i] - case "another-plugin": - anotherPlugin = &discoveries[i] - } - } - - Expect(testPlugin).NotTo(BeNil()) - Expect(testPlugin.Error).To(BeNil()) - Expect(testPlugin.Manifest.Name).To(Equal("Test Plugin")) - Expect(string(testPlugin.Manifest.Capabilities[0])).To(Equal("MetadataAgent")) - - Expect(anotherPlugin).NotTo(BeNil()) - Expect(anotherPlugin.Error).To(BeNil()) - Expect(anotherPlugin.Manifest.Name).To(Equal("Another Plugin")) - Expect(string(anotherPlugin.Manifest.Capabilities[0])).To(Equal("Scrobbler")) - }) - - It("should handle plugins with same manifest name in different directories", func() { - createValidPlugin("lastfm-official", "lastfm", "Official Author", "1.0.0", []string{"MetadataAgent"}) - createValidPlugin("lastfm-custom", "lastfm", "Custom Author", "2.0.0", []string{"MetadataAgent"}) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(2)) - - // Find each plugin by ID - var officialPlugin, customPlugin *PluginDiscoveryEntry - for i := range discoveries { - switch discoveries[i].ID { - case "lastfm-official": - officialPlugin = &discoveries[i] - case "lastfm-custom": - customPlugin = &discoveries[i] - } - } - - Expect(officialPlugin).NotTo(BeNil()) - Expect(officialPlugin.Error).To(BeNil()) - Expect(officialPlugin.Manifest.Name).To(Equal("lastfm")) - Expect(officialPlugin.Manifest.Author).To(Equal("Official Author")) - - Expect(customPlugin).NotTo(BeNil()) - Expect(customPlugin.Error).To(BeNil()) - Expect(customPlugin.Manifest.Name).To(Equal("lastfm")) - Expect(customPlugin.Manifest.Author).To(Equal("Custom Author")) - }) - }) - - Context("Missing files", func() { - It("should report error for plugins missing WASM files", func() { - createManifestOnlyPlugin("manifest-only") - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("manifest-only")) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("no plugin.wasm found")) - }) - - It("should skip directories missing manifest files", func() { - createWasmOnlyPlugin("wasm-only") - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("wasm-only")) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("failed to load manifest")) - }) - }) - - Context("Invalid content", func() { - It("should report error for invalid manifest JSON", func() { - createInvalidManifestPlugin("invalid-manifest") - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("invalid-manifest")) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("failed to load manifest")) - }) - - It("should report error for plugins with empty capabilities", func() { - createEmptyCapabilitiesPlugin("empty-capabilities") - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("empty-capabilities")) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("field capabilities length: must be >= 1")) - }) - }) - - Context("Symlinks", func() { - It("should discover symlinked plugins correctly", func() { - // Create a real plugin directory outside tempPluginsDir - realPluginDir, err := os.MkdirTemp("", "navidrome-real-plugin-*") - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { - _ = os.RemoveAll(realPluginDir) - }) - - // Create plugin files in the real directory - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(realPluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - - manifest := `{ - "name": "symlinked-plugin", - "version": "1.0.0", - "capabilities": ["MetadataAgent"], - "author": "Test Author", - "description": "Test Plugin", - "website": "https://test.navidrome.org/symlinked-plugin", - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(realPluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - - // Create symlink - symlinkPath := filepath.Join(tempPluginsDir, "symlinked-plugin") - Expect(os.Symlink(realPluginDir, symlinkPath)).To(Succeed()) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("symlinked-plugin")) - Expect(discoveries[0].Error).To(BeNil()) - Expect(discoveries[0].IsSymlink).To(BeTrue()) - Expect(discoveries[0].Path).To(Equal(realPluginDir)) - Expect(discoveries[0].Manifest.Name).To(Equal("symlinked-plugin")) - }) - - It("should handle relative symlinks", func() { - // Create a real plugin directory in the same parent as tempPluginsDir - parentDir := filepath.Dir(tempPluginsDir) - realPluginDir := filepath.Join(parentDir, "real-plugin-dir") - Expect(os.MkdirAll(realPluginDir, 0755)).To(Succeed()) - DeferCleanup(func() { - _ = os.RemoveAll(realPluginDir) - }) - - // Create plugin files in the real directory - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(realPluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - - manifest := `{ - "name": "relative-symlinked-plugin", - "version": "1.0.0", - "capabilities": ["MetadataAgent"], - "author": "Test Author", - "description": "Test Plugin", - "website": "https://test.navidrome.org/relative-symlinked-plugin", - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(realPluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - - // Create relative symlink - symlinkPath := filepath.Join(tempPluginsDir, "relative-symlinked-plugin") - relativeTarget := "../real-plugin-dir" - Expect(os.Symlink(relativeTarget, symlinkPath)).To(Succeed()) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("relative-symlinked-plugin")) - Expect(discoveries[0].Error).To(BeNil()) - Expect(discoveries[0].IsSymlink).To(BeTrue()) - Expect(discoveries[0].Path).To(Equal(realPluginDir)) - Expect(discoveries[0].Manifest.Name).To(Equal("relative-symlinked-plugin")) - }) - - It("should report error for broken symlinks", func() { - symlinkPath := filepath.Join(tempPluginsDir, "broken-symlink") - nonExistentTarget := "/non/existent/path" - Expect(os.Symlink(nonExistentTarget, symlinkPath)).To(Succeed()) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("broken-symlink")) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("failed to stat symlink target")) - Expect(discoveries[0].IsSymlink).To(BeTrue()) - }) - - It("should report error for symlinks pointing to files", func() { - // Create a regular file - regularFile := filepath.Join(tempPluginsDir, "regular-file.txt") - Expect(os.WriteFile(regularFile, []byte("content"), 0600)).To(Succeed()) - - // Create symlink pointing to the file - symlinkPath := filepath.Join(tempPluginsDir, "symlink-to-file") - Expect(os.Symlink(regularFile, symlinkPath)).To(Succeed()) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("symlink-to-file")) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("symlink target is not a directory")) - Expect(discoveries[0].IsSymlink).To(BeTrue()) - }) - }) - - Context("Directory filtering", func() { - It("should ignore hidden directories", func() { - createValidPlugin(".hidden-plugin", "Hidden Plugin", "Test Author", "1.0.0", []string{"MetadataAgent"}) - createValidPlugin("visible-plugin", "Visible Plugin", "Test Author", "1.0.0", []string{"MetadataAgent"}) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("visible-plugin")) - }) - - It("should ignore regular files", func() { - // Create a regular file - Expect(os.WriteFile(filepath.Join(tempPluginsDir, "regular-file.txt"), []byte("content"), 0600)).To(Succeed()) - createValidPlugin("valid-plugin", "Valid Plugin", "Test Author", "1.0.0", []string{"MetadataAgent"}) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].ID).To(Equal("valid-plugin")) - }) - - It("should handle mixed valid and invalid plugins", func() { - createValidPlugin("valid-plugin", "Valid Plugin", "Test Author", "1.0.0", []string{"MetadataAgent"}) - createManifestOnlyPlugin("manifest-only") - createInvalidManifestPlugin("invalid-manifest") - createValidPlugin("another-valid", "Another Valid", "Test Author", "1.0.0", []string{"Scrobbler"}) - - discoveries := DiscoverPlugins(tempPluginsDir) - - Expect(discoveries).To(HaveLen(4)) - - var validCount int - var errorCount int - for _, discovery := range discoveries { - if discovery.Error == nil { - validCount++ - } else { - errorCount++ - } - } - - Expect(validCount).To(Equal(2)) - Expect(errorCount).To(Equal(2)) - }) - }) - - Context("Error handling", func() { - It("should handle non-existent plugins directory", func() { - nonExistentDir := "/non/existent/plugins/dir" - - discoveries := DiscoverPlugins(nonExistentDir) - - Expect(discoveries).To(HaveLen(1)) - Expect(discoveries[0].Error).To(HaveOccurred()) - Expect(discoveries[0].Error.Error()).To(ContainSubstring("failed to read plugins directory")) - }) - }) -}) diff --git a/plugins/examples/Makefile b/plugins/examples/Makefile deleted file mode 100644 index e2acc2ff8..000000000 --- a/plugins/examples/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -all: wikimedia coverartarchive crypto-ticker discord-rich-presence subsonicapi-demo - -wikimedia: wikimedia/plugin.wasm -coverartarchive: coverartarchive/plugin.wasm -crypto-ticker: crypto-ticker/plugin.wasm -discord-rich-presence: discord-rich-presence/plugin.wasm -subsonicapi-demo: subsonicapi-demo/plugin.wasm - -wikimedia/plugin.wasm: wikimedia/plugin.go - GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./wikimedia - -coverartarchive/plugin.wasm: coverartarchive/plugin.go - GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./coverartarchive - -crypto-ticker/plugin.wasm: crypto-ticker/plugin.go - GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./crypto-ticker - -DISCORD_RP_FILES=$(shell find discord-rich-presence -type f -name "*.go") -discord-rich-presence/plugin.wasm: $(DISCORD_RP_FILES) - GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./discord-rich-presence/... - -subsonicapi-demo/plugin.wasm: subsonicapi-demo/plugin.go - GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./subsonicapi-demo - -clean: - rm -f wikimedia/plugin.wasm coverartarchive/plugin.wasm crypto-ticker/plugin.wasm \ - discord-rich-presence/plugin.wasm subsonicapi-demo/plugin.wasm \ No newline at end of file diff --git a/plugins/examples/README.md b/plugins/examples/README.md deleted file mode 100644 index 61d6b2ef9..000000000 --- a/plugins/examples/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Plugin Examples - -This directory contains example plugins for Navidrome, intended for demonstration and reference purposes. These plugins are not used in automated tests. - -## Contents - -- `wikimedia/`: Retrieves artist information from Wikidata. -- `coverartarchive/`: Fetches album cover images from the Cover Art Archive. -- `crypto-ticker/`: Uses websockets to log real-time cryptocurrency prices. -- `discord-rich-presence/`: Integrates with Discord Rich Presence to display currently playing tracks on Discord profiles. -- `subsonicapi-demo/`: Demonstrates interaction with Navidrome's Subsonic API from a plugin. - -## Building - -To build all example plugins, run: - -``` -make -``` - -Or to build a specific plugin: - -``` -make wikimedia -make coverartarchive -make crypto-ticker -make discord-rich-presence -make subsonicapi-demo -``` - -This will produce the corresponding `plugin.wasm` files in each plugin's directory. diff --git a/plugins/examples/coverartarchive/README.md b/plugins/examples/coverartarchive/README.md deleted file mode 100644 index e886f6871..000000000 --- a/plugins/examples/coverartarchive/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Cover Art Archive AlbumMetadataService Plugin - -This plugin provides album cover images for Navidrome by querying the [Cover Art Archive](https://coverartarchive.org/) API using the MusicBrainz Release Group MBID. - -## Features - -- Implements only the `GetAlbumImages` method of the AlbumMetadataService plugin interface. -- Returns front cover images for a given release-group MBID. -- Returns `not found` if no MBID is provided or no images are found. - -## Requirements - -- Go 1.24 or newer (with WASI support) -- The Navidrome repository (with generated plugin API code in `plugins/api`) - -## How to Compile - -To build the WASM plugin, run the following command from the project root: - -```sh -GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugins/testdata/coverartarchive/plugin.wasm ./plugins/testdata/coverartarchive -``` - -This will produce `plugin.wasm` in this directory. - -## Usage - -- The plugin can be loaded by Navidrome for integration and end-to-end tests of the plugin system. -- It is intended for testing and development purposes only. - -## API Reference - -- [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API) -- This plugin uses the endpoint: `https://coverartarchive.org/release-group/{mbid}` diff --git a/plugins/examples/coverartarchive/manifest.json b/plugins/examples/coverartarchive/manifest.json deleted file mode 100644 index 4049fc358..000000000 --- a/plugins/examples/coverartarchive/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json", - "name": "coverartarchive", - "author": "Navidrome", - "version": "1.0.0", - "description": "Album cover art from the Cover Art Archive", - "website": "https://coverartarchive.org", - "capabilities": ["MetadataAgent"], - "permissions": { - "http": { - "reason": "To fetch album cover art from the Cover Art Archive API", - "allowedUrls": { - "https://coverartarchive.org": ["GET"], - "https://*.archive.org": ["GET"] - }, - "allowLocalNetwork": false - } - } -} diff --git a/plugins/examples/coverartarchive/plugin.go b/plugins/examples/coverartarchive/plugin.go deleted file mode 100644 index ee612c31c..000000000 --- a/plugins/examples/coverartarchive/plugin.go +++ /dev/null @@ -1,151 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/http" -) - -type CoverArtArchiveAgent struct{} - -var ErrNotFound = api.ErrNotFound - -type caaImage struct { - Image string `json:"image"` - Front bool `json:"front"` - Types []string `json:"types"` - Thumbnails map[string]string `json:"thumbnails"` -} - -var client = http.NewHttpService() - -func (CoverArtArchiveAgent) GetAlbumImages(ctx context.Context, req *api.AlbumImagesRequest) (*api.AlbumImagesResponse, error) { - if req.Mbid == "" { - return nil, ErrNotFound - } - - url := "https://coverartarchive.org/release/" + req.Mbid - resp, err := client.Get(ctx, &http.HttpRequest{Url: url, TimeoutMs: 5000}) - if err != nil || resp.Status != 200 { - log.Printf("[CAA] Error getting album images from CoverArtArchive (status: %d): %v", resp.Status, err) - return nil, ErrNotFound - } - - images, err := extractFrontImages(resp.Body) - if err != nil || len(images) == 0 { - return nil, ErrNotFound - } - return &api.AlbumImagesResponse{Images: images}, nil -} - -func extractFrontImages(body []byte) ([]*api.ExternalImage, error) { - var data struct { - Images []caaImage `json:"images"` - } - if err := json.Unmarshal(body, &data); err != nil { - return nil, err - } - img := findFrontImage(data.Images) - if img == nil { - return nil, ErrNotFound - } - return buildImageList(img), nil -} - -func findFrontImage(images []caaImage) *caaImage { - for i, img := range images { - if img.Front { - return &images[i] - } - } - for i, img := range images { - for _, t := range img.Types { - if t == "Front" { - return &images[i] - } - } - } - if len(images) > 0 { - return &images[0] - } - return nil -} - -func buildImageList(img *caaImage) []*api.ExternalImage { - var images []*api.ExternalImage - // First, try numeric sizes only - for sizeStr, url := range img.Thumbnails { - if url == "" { - continue - } - size := 0 - if _, err := fmt.Sscanf(sizeStr, "%d", &size); err == nil { - images = append(images, &api.ExternalImage{Url: url, Size: int32(size)}) - } - } - // If no numeric sizes, fallback to large/small - if len(images) == 0 { - for sizeStr, url := range img.Thumbnails { - if url == "" { - continue - } - var size int - switch sizeStr { - case "large": - size = 500 - case "small": - size = 250 - default: - continue - } - images = append(images, &api.ExternalImage{Url: url, Size: int32(size)}) - } - } - if len(images) == 0 && img.Image != "" { - images = append(images, &api.ExternalImage{Url: img.Image, Size: 0}) - } - return images -} - -func (CoverArtArchiveAgent) GetAlbumInfo(ctx context.Context, req *api.AlbumInfoRequest) (*api.AlbumInfoResponse, error) { - return nil, api.ErrNotImplemented -} -func (CoverArtArchiveAgent) GetArtistMBID(ctx context.Context, req *api.ArtistMBIDRequest) (*api.ArtistMBIDResponse, error) { - return nil, api.ErrNotImplemented -} - -func (CoverArtArchiveAgent) GetArtistURL(ctx context.Context, req *api.ArtistURLRequest) (*api.ArtistURLResponse, error) { - return nil, api.ErrNotImplemented -} - -func (CoverArtArchiveAgent) GetArtistBiography(ctx context.Context, req *api.ArtistBiographyRequest) (*api.ArtistBiographyResponse, error) { - return nil, api.ErrNotImplemented -} - -func (CoverArtArchiveAgent) GetSimilarArtists(ctx context.Context, req *api.ArtistSimilarRequest) (*api.ArtistSimilarResponse, error) { - return nil, api.ErrNotImplemented -} - -func (CoverArtArchiveAgent) GetArtistImages(ctx context.Context, req *api.ArtistImageRequest) (*api.ArtistImageResponse, error) { - return nil, api.ErrNotImplemented -} - -func (CoverArtArchiveAgent) GetArtistTopSongs(ctx context.Context, req *api.ArtistTopSongsRequest) (*api.ArtistTopSongsResponse, error) { - return nil, api.ErrNotImplemented -} - -func main() {} - -func init() { - // Configure logging: No timestamps, no source file/line - log.SetFlags(0) - log.SetPrefix("[CAA] ") - - api.RegisterMetadataAgent(CoverArtArchiveAgent{}) -} diff --git a/plugins/examples/crypto-ticker/README.md b/plugins/examples/crypto-ticker/README.md deleted file mode 100644 index ca6d2c44a..000000000 --- a/plugins/examples/crypto-ticker/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Crypto Ticker Plugin - -This is a WebSocket-based WASM plugin for Navidrome that displays real-time cryptocurrency prices from Coinbase. - -## Features - -- Connects to Coinbase WebSocket API to receive real-time ticker updates -- Configurable to track multiple cryptocurrency pairs -- Implements WebSocketCallback and LifecycleManagement interfaces -- Automatically reconnects on connection loss -- Displays price, best bid, best ask, and 24-hour percentage change - -## Configuration - -In your `navidrome.toml` file, add: - -```toml -[PluginConfig.crypto-ticker] -tickers = "BTC,ETH,SOL,MATIC" -``` - -- `tickers` is a comma-separated list of cryptocurrency symbols -- The plugin will append `-USD` to any symbol without a trading pair specified - -## How it Works - -- The plugin connects to Coinbase's WebSocket API upon initialization -- It subscribes to ticker updates for the configured cryptocurrencies -- Incoming ticker data is processed and logged -- On connection loss, it automatically attempts to reconnect (TODO) - -## Building - -To build the plugin to WASM: - -``` -GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm plugin.go -``` - -## Installation - -Copy the resulting `plugin.wasm` and create a `manifest.json` file in your Navidrome plugins folder under a `crypto-ticker` directory. - -## Example Output - -``` -CRYPTO TICKER: BTC-USD Price: 65432.50 Best Bid: 65431.25 Best Ask: 65433.75 24h Change: 2.75% -CRYPTO TICKER: ETH-USD Price: 3456.78 Best Bid: 3455.90 Best Ask: 3457.80 24h Change: 1.25% -``` - ---- - -For more details, see the source code in `plugin.go`. diff --git a/plugins/examples/crypto-ticker/manifest.json b/plugins/examples/crypto-ticker/manifest.json deleted file mode 100644 index 482731684..000000000 --- a/plugins/examples/crypto-ticker/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "crypto-ticker", - "author": "Navidrome Plugin", - "version": "1.0.0", - "description": "A plugin that tracks crypto currency prices using Coinbase WebSocket API", - "website": "https://github.com/navidrome/navidrome/tree/master/plugins/examples/crypto-ticker", - "capabilities": [ - "WebSocketCallback", - "LifecycleManagement", - "SchedulerCallback" - ], - "permissions": { - "config": { - "reason": "To read API configuration and WebSocket endpoint settings" - }, - "scheduler": { - "reason": "To schedule periodic reconnection attempts and status updates" - }, - "websocket": { - "reason": "To connect to Coinbase WebSocket API for real-time cryptocurrency prices", - "allowedUrls": ["wss://ws-feed.exchange.coinbase.com"], - "allowLocalNetwork": false - } - } -} diff --git a/plugins/examples/crypto-ticker/plugin.go b/plugins/examples/crypto-ticker/plugin.go deleted file mode 100644 index 3fced6d5c..000000000 --- a/plugins/examples/crypto-ticker/plugin.go +++ /dev/null @@ -1,304 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "strings" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/config" - "github.com/navidrome/navidrome/plugins/host/scheduler" - "github.com/navidrome/navidrome/plugins/host/websocket" -) - -const ( - // Coinbase WebSocket API endpoint - coinbaseWSEndpoint = "wss://ws-feed.exchange.coinbase.com" - - // Connection ID for our WebSocket connection - connectionID = "crypto-ticker-connection" - - // ID for the reconnection schedule - reconnectScheduleID = "crypto-ticker-reconnect" -) - -var ( - // Store ticker symbols from the configuration - tickers []string -) - -// WebSocketService instance used to manage WebSocket connections and communication. -var wsService = websocket.NewWebSocketService() - -// ConfigService instance for accessing plugin configuration. -var configService = config.NewConfigService() - -// SchedulerService instance for scheduling tasks. -var schedService = scheduler.NewSchedulerService() - -// CryptoTickerPlugin implements WebSocketCallback, LifecycleManagement, and SchedulerCallback interfaces -type CryptoTickerPlugin struct{} - -// Coinbase subscription message structure -type CoinbaseSubscription struct { - Type string `json:"type"` - ProductIDs []string `json:"product_ids"` - Channels []string `json:"channels"` -} - -// Coinbase ticker message structure -type CoinbaseTicker struct { - Type string `json:"type"` - Sequence int64 `json:"sequence"` - ProductID string `json:"product_id"` - Price string `json:"price"` - Open24h string `json:"open_24h"` - Volume24h string `json:"volume_24h"` - Low24h string `json:"low_24h"` - High24h string `json:"high_24h"` - Volume30d string `json:"volume_30d"` - BestBid string `json:"best_bid"` - BestAsk string `json:"best_ask"` - Side string `json:"side"` - Time string `json:"time"` - TradeID int `json:"trade_id"` - LastSize string `json:"last_size"` -} - -// OnInit is called when the plugin is loaded -func (CryptoTickerPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { - log.Printf("Crypto Ticker Plugin initializing...") - - // Check if ticker configuration exists - tickerConfig, ok := req.Config["tickers"] - if !ok { - return &api.InitResponse{Error: "Missing 'tickers' configuration"}, nil - } - - // Parse ticker symbols - tickers := parseTickerSymbols(tickerConfig) - log.Printf("Configured tickers: %v", tickers) - - // Connect to WebSocket and subscribe to tickers - err := connectAndSubscribe(ctx, tickers) - if err != nil { - return &api.InitResponse{Error: err.Error()}, nil - } - - return &api.InitResponse{}, nil -} - -// Helper function to parse ticker symbols from a comma-separated string -func parseTickerSymbols(tickerConfig string) []string { - tickers := strings.Split(tickerConfig, ",") - for i, ticker := range tickers { - tickers[i] = strings.TrimSpace(ticker) - - // Add -USD suffix if not present - if !strings.Contains(tickers[i], "-") { - tickers[i] = tickers[i] + "-USD" - } - } - return tickers -} - -// Helper function to connect to WebSocket and subscribe to tickers -func connectAndSubscribe(ctx context.Context, tickers []string) error { - // Connect to the WebSocket API - _, err := wsService.Connect(ctx, &websocket.ConnectRequest{ - Url: coinbaseWSEndpoint, - ConnectionId: connectionID, - }) - - if err != nil { - log.Printf("Failed to connect to Coinbase WebSocket API: %v", err) - return fmt.Errorf("WebSocket connection error: %v", err) - } - - log.Printf("Connected to Coinbase WebSocket API") - - // Subscribe to ticker channel for the configured symbols - subscription := CoinbaseSubscription{ - Type: "subscribe", - ProductIDs: tickers, - Channels: []string{"ticker"}, - } - - subscriptionJSON, err := json.Marshal(subscription) - if err != nil { - log.Printf("Failed to marshal subscription message: %v", err) - return fmt.Errorf("JSON marshal error: %v", err) - } - - // Send subscription message - _, err = wsService.SendText(ctx, &websocket.SendTextRequest{ - ConnectionId: connectionID, - Message: string(subscriptionJSON), - }) - - if err != nil { - log.Printf("Failed to send subscription message: %v", err) - return fmt.Errorf("WebSocket send error: %v", err) - } - - log.Printf("Subscription message sent to Coinbase WebSocket API") - return nil -} - -// OnTextMessage is called when a text message is received from the WebSocket -func (CryptoTickerPlugin) OnTextMessage(ctx context.Context, req *api.OnTextMessageRequest) (*api.OnTextMessageResponse, error) { - // Only process messages from our connection - if req.ConnectionId != connectionID { - log.Printf("Received message from unexpected connection: %s", req.ConnectionId) - return &api.OnTextMessageResponse{}, nil - } - - // Try to parse as a ticker message - var ticker CoinbaseTicker - err := json.Unmarshal([]byte(req.Message), &ticker) - if err != nil { - log.Printf("Failed to parse ticker message: %v", err) - return &api.OnTextMessageResponse{}, nil - } - - // If the message is not a ticker or has an error, just log it - if ticker.Type != "ticker" { - // This could be subscription confirmation or other messages - log.Printf("Received non-ticker message: %s", req.Message) - return &api.OnTextMessageResponse{}, nil - } - - // Format and print ticker information - log.Printf("CRYPTO TICKER: %s Price: %s Best Bid: %s Best Ask: %s 24h Change: %s%%\n", - ticker.ProductID, - ticker.Price, - ticker.BestBid, - ticker.BestAsk, - calculatePercentChange(ticker.Open24h, ticker.Price), - ) - - return &api.OnTextMessageResponse{}, nil -} - -// OnBinaryMessage is called when a binary message is received -func (CryptoTickerPlugin) OnBinaryMessage(ctx context.Context, req *api.OnBinaryMessageRequest) (*api.OnBinaryMessageResponse, error) { - // Not expected from Coinbase WebSocket API - return &api.OnBinaryMessageResponse{}, nil -} - -// OnError is called when an error occurs on the WebSocket connection -func (CryptoTickerPlugin) OnError(ctx context.Context, req *api.OnErrorRequest) (*api.OnErrorResponse, error) { - log.Printf("WebSocket error: %s", req.Error) - return &api.OnErrorResponse{}, nil -} - -// OnClose is called when the WebSocket connection is closed -func (CryptoTickerPlugin) OnClose(ctx context.Context, req *api.OnCloseRequest) (*api.OnCloseResponse, error) { - log.Printf("WebSocket connection closed with code %d: %s", req.Code, req.Reason) - - // Try to reconnect if this is our connection - if req.ConnectionId == connectionID { - log.Printf("Scheduling reconnection attempts every 2 seconds...") - - // Create a recurring schedule to attempt reconnection every 2 seconds - resp, err := schedService.ScheduleRecurring(ctx, &scheduler.ScheduleRecurringRequest{ - // Run every 2 seconds using cron expression - CronExpression: "*/2 * * * * *", - ScheduleId: reconnectScheduleID, - }) - - if err != nil { - log.Printf("Failed to schedule reconnection attempts: %v", err) - } else { - log.Printf("Reconnection schedule created with ID: %s", resp.ScheduleId) - } - } - - return &api.OnCloseResponse{}, nil -} - -// OnSchedulerCallback is called when a scheduled event triggers -func (CryptoTickerPlugin) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) { - // Only handle our reconnection schedule - if req.ScheduleId != reconnectScheduleID { - log.Printf("Received callback for unknown schedule: %s", req.ScheduleId) - return &api.SchedulerCallbackResponse{}, nil - } - - log.Printf("Attempting to reconnect to Coinbase WebSocket API...") - - // Get the current ticker configuration - configResp, err := configService.GetPluginConfig(ctx, &config.GetPluginConfigRequest{}) - if err != nil { - log.Printf("Failed to get plugin configuration: %v", err) - return &api.SchedulerCallbackResponse{Error: fmt.Sprintf("Config error: %v", err)}, nil - } - - // Check if ticker configuration exists - tickerConfig, ok := configResp.Config["tickers"] - if !ok { - log.Printf("Missing 'tickers' configuration") - return &api.SchedulerCallbackResponse{Error: "Missing 'tickers' configuration"}, nil - } - - // Parse ticker symbols - tickers := parseTickerSymbols(tickerConfig) - log.Printf("Reconnecting with tickers: %v", tickers) - - // Try to connect and subscribe - err = connectAndSubscribe(ctx, tickers) - if err != nil { - log.Printf("Reconnection attempt failed: %v", err) - return &api.SchedulerCallbackResponse{Error: err.Error()}, nil - } - - // Successfully reconnected, cancel the reconnection schedule - _, err = schedService.CancelSchedule(ctx, &scheduler.CancelRequest{ - ScheduleId: reconnectScheduleID, - }) - - if err != nil { - log.Printf("Failed to cancel reconnection schedule: %v", err) - } else { - log.Printf("Reconnection schedule canceled after successful reconnection") - } - - return &api.SchedulerCallbackResponse{}, nil -} - -// Helper function to calculate percent change -func calculatePercentChange(open, current string) string { - var openFloat, currentFloat float64 - _, err := fmt.Sscanf(open, "%f", &openFloat) - if err != nil { - return "N/A" - } - _, err = fmt.Sscanf(current, "%f", ¤tFloat) - if err != nil { - return "N/A" - } - - if openFloat == 0 { - return "N/A" - } - - change := ((currentFloat - openFloat) / openFloat) * 100 - return fmt.Sprintf("%.2f", change) -} - -// Required by Go WASI build -func main() {} - -func init() { - // Configure logging: No timestamps, no source file/line, prepend [Crypto] - log.SetFlags(0) - log.SetPrefix("[Crypto] ") - - api.RegisterWebSocketCallback(CryptoTickerPlugin{}) - api.RegisterLifecycleManagement(CryptoTickerPlugin{}) - api.RegisterSchedulerCallback(CryptoTickerPlugin{}) -} diff --git a/plugins/examples/discord-rich-presence/README.md b/plugins/examples/discord-rich-presence/README.md deleted file mode 100644 index 80b12166f..000000000 --- a/plugins/examples/discord-rich-presence/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Discord Rich Presence Plugin - -This example plugin integrates Navidrome with Discord Rich Presence. It shows how a plugin can keep a real-time -connection to an external service while remaining completely stateless. This plugin is based on the -[Navicord](https://github.com/logixism/navicord) project, which provides a similar functionality. - -**NOTE: This plugin is for demonstration purposes only. It relies on the user's Discord token being stored in the -Navidrome configuration file, which is not secure, and may be against Discord's terms of service. -Use it at your own risk.** - -## Overview - -The plugin exposes three capabilities: - -- **Scrobbler** – receives `NowPlaying` notifications from Navidrome -- **WebSocketCallback** – handles Discord gateway messages -- **SchedulerCallback** – used to clear presence and send periodic heartbeats - -It relies on several host services declared in `manifest.json`: - -- `http` – queries Discord API endpoints -- `websocket` – maintains gateway connections -- `scheduler` – schedules heartbeats and presence cleanup -- `cache` – stores sequence numbers for heartbeats -- `config` – retrieves the plugin configuration on each call -- `artwork` – resolves track artwork URLs - -## Architecture - -Each call from Navidrome creates a new plugin instance. The `init` function registers the capabilities and obtains the -scheduler service: - -```go -api.RegisterScrobbler(plugin) -api.RegisterWebSocketCallback(plugin.rpc) -plugin.sched = api.RegisterNamedSchedulerCallback("close-activity", plugin) -plugin.rpc.sched = api.RegisterNamedSchedulerCallback("heartbeat", plugin.rpc) -``` - -When `NowPlaying` is invoked the plugin: - -1. Loads `clientid` and user tokens from the configuration (because plugins are stateless). -2. Connects to Discord using `WebSocketService` if no connection exists. -3. Sends the activity payload with track details and artwork. -4. Schedules a one‑time callback to clear the presence after the track finishes. - -Heartbeat messages are sent by a recurring scheduler job. Sequence numbers received from Discord are stored in -`CacheService` to remain available across plugin instances. - -The `OnSchedulerCallback` method clears the presence and closes the connection when the scheduled time is reached. - -```go -// The plugin is stateless, we need to load the configuration every time -clientID, users, err := d.getConfig(ctx) -``` - -## Configuration - -Add the following to `navidrome.toml` and adjust for your tokens: - -```toml -[PluginConfig.discord-rich-presence] -ClientID = "123456789012345678" -Users = "alice:token123,bob:token456" -``` - -- `clientid` is your Discord application ID -- `users` is a comma‑separated list of `username:token` pairs used for authorization - -## Building - -```sh -GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm ./discord-rich-presence/... -``` - -Place the resulting `plugin.wasm` and `manifest.json` in a `discord-rich-presence` folder under your Navidrome plugins -directory. - -## Stateless Operation - -Navidrome plugins are completely stateless – each method call instantiates a new plugin instance and discards it -afterwards. - -To work within this model the plugin stores no in-memory state. Connections are keyed by user name inside the host -services and any transient data (like Discord sequence numbers) is kept in the cache. Configuration is reloaded on every -method call. - -For more implementation details see `plugin.go` and `rpc.go`. diff --git a/plugins/examples/discord-rich-presence/manifest.json b/plugins/examples/discord-rich-presence/manifest.json deleted file mode 100644 index c6fa9c283..000000000 --- a/plugins/examples/discord-rich-presence/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json", - "name": "discord-rich-presence", - "author": "Navidrome Team", - "version": "1.0.0", - "description": "Discord Rich Presence integration for Navidrome", - "website": "https://github.com/navidrome/navidrome/tree/master/plugins/examples/discord-rich-presence", - "capabilities": ["Scrobbler", "SchedulerCallback", "WebSocketCallback"], - "permissions": { - "http": { - "reason": "To communicate with Discord API for gateway discovery and image uploads", - "allowedUrls": { - "https://discord.com/api/*": ["GET", "POST"] - }, - "allowLocalNetwork": false - }, - "websocket": { - "reason": "To maintain real-time connection with Discord gateway", - "allowedUrls": ["wss://gateway.discord.gg"], - "allowLocalNetwork": false - }, - "config": { - "reason": "To access plugin configuration (client ID and user tokens)" - }, - "cache": { - "reason": "To store connection state and sequence numbers" - }, - "scheduler": { - "reason": "To schedule heartbeat messages and activity clearing" - }, - "artwork": { - "reason": "To get track artwork URLs for rich presence display" - } - } -} diff --git a/plugins/examples/discord-rich-presence/plugin.go b/plugins/examples/discord-rich-presence/plugin.go deleted file mode 100644 index c93ccf35d..000000000 --- a/plugins/examples/discord-rich-presence/plugin.go +++ /dev/null @@ -1,186 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "strings" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/artwork" - "github.com/navidrome/navidrome/plugins/host/cache" - "github.com/navidrome/navidrome/plugins/host/config" - "github.com/navidrome/navidrome/plugins/host/http" - "github.com/navidrome/navidrome/plugins/host/scheduler" - "github.com/navidrome/navidrome/plugins/host/websocket" - "github.com/navidrome/navidrome/utils/slice" -) - -type DiscordRPPlugin struct { - rpc *discordRPC - cfg config.ConfigService - artwork artwork.ArtworkService - sched scheduler.SchedulerService -} - -func (d *DiscordRPPlugin) IsAuthorized(ctx context.Context, req *api.ScrobblerIsAuthorizedRequest) (*api.ScrobblerIsAuthorizedResponse, error) { - // Get plugin configuration - _, users, err := d.getConfig(ctx) - if err != nil { - return nil, fmt.Errorf("failed to check user authorization: %w", err) - } - - // Check if the user has a Discord token configured - _, authorized := users[req.Username] - log.Printf("IsAuthorized for user %s: %v", req.Username, authorized) - return &api.ScrobblerIsAuthorizedResponse{ - Authorized: authorized, - }, nil -} - -func (d *DiscordRPPlugin) NowPlaying(ctx context.Context, request *api.ScrobblerNowPlayingRequest) (*api.ScrobblerNowPlayingResponse, error) { - log.Printf("Setting presence for user %s, track: %s", request.Username, request.Track.Name) - - // The plugin is stateless, we need to load the configuration every time - clientID, users, err := d.getConfig(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get config: %w", err) - } - - // Check if the user has a Discord token configured - userToken, authorized := users[request.Username] - if !authorized { - return nil, fmt.Errorf("user '%s' not authorized", request.Username) - } - - // Make sure we have a connection - if err := d.rpc.connect(ctx, request.Username, userToken); err != nil { - return nil, fmt.Errorf("failed to connect to Discord: %w", err) - } - - // Cancel any existing completion schedule - if resp, _ := d.sched.CancelSchedule(ctx, &scheduler.CancelRequest{ScheduleId: request.Username}); resp.Error != "" { - log.Printf("Ignoring failure to cancel schedule: %s", resp.Error) - } - - // Send activity update - if err := d.rpc.sendActivity(ctx, clientID, request.Username, userToken, activity{ - Application: clientID, - Name: "Navidrome", - Type: 2, - Details: request.Track.Name, - State: d.getArtistList(request.Track), - Timestamps: activityTimestamps{ - Start: (request.Timestamp - int64(request.Track.Position)) * 1000, - End: (request.Timestamp - int64(request.Track.Position) + int64(request.Track.Length)) * 1000, - }, - Assets: activityAssets{ - LargeImage: d.imageURL(ctx, request), - LargeText: request.Track.Album, - }, - }); err != nil { - return nil, fmt.Errorf("failed to send activity: %w", err) - } - - // Schedule a timer to clear the activity after the track completes - _, err = d.sched.ScheduleOneTime(ctx, &scheduler.ScheduleOneTimeRequest{ - ScheduleId: request.Username, - DelaySeconds: request.Track.Length - request.Track.Position + 5, - }) - if err != nil { - return nil, fmt.Errorf("failed to schedule completion timer: %w", err) - } - - return nil, nil -} - -func (d *DiscordRPPlugin) imageURL(ctx context.Context, request *api.ScrobblerNowPlayingRequest) string { - imageResp, _ := d.artwork.GetTrackUrl(ctx, &artwork.GetArtworkUrlRequest{Id: request.Track.Id, Size: 300}) - imageURL := imageResp.Url - if strings.HasPrefix(imageURL, "http://localhost") { - return "" - } - return imageURL -} - -func (d *DiscordRPPlugin) getArtistList(track *api.TrackInfo) string { - return strings.Join(slice.Map(track.Artists, func(a *api.Artist) string { return a.Name }), " • ") -} - -func (d *DiscordRPPlugin) Scrobble(context.Context, *api.ScrobblerScrobbleRequest) (*api.ScrobblerScrobbleResponse, error) { - return nil, nil -} - -func (d *DiscordRPPlugin) getConfig(ctx context.Context) (string, map[string]string, error) { - const ( - clientIDKey = "clientid" - usersKey = "users" - ) - confResp, err := d.cfg.GetPluginConfig(ctx, &config.GetPluginConfigRequest{}) - if err != nil { - return "", nil, fmt.Errorf("unable to load config: %w", err) - } - conf := confResp.GetConfig() - if len(conf) < 1 { - log.Print("missing configuration") - return "", nil, nil - } - clientID := conf[clientIDKey] - if clientID == "" { - log.Printf("missing ClientID: %v", conf) - return "", nil, nil - } - cfgUsers := conf[usersKey] - if len(cfgUsers) == 0 { - log.Print("no users configured") - return "", nil, nil - } - users := map[string]string{} - for _, user := range strings.Split(cfgUsers, ",") { - tuple := strings.Split(user, ":") - if len(tuple) != 2 { - return clientID, nil, fmt.Errorf("invalid user config: %s", user) - } - users[tuple[0]] = tuple[1] - } - return clientID, users, nil -} - -func (d *DiscordRPPlugin) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) { - log.Printf("Removing presence for user %s", req.ScheduleId) - if err := d.rpc.clearActivity(ctx, req.ScheduleId); err != nil { - return nil, fmt.Errorf("failed to clear activity: %w", err) - } - log.Printf("Disconnecting user %s", req.ScheduleId) - if err := d.rpc.disconnect(ctx, req.ScheduleId); err != nil { - return nil, fmt.Errorf("failed to disconnect from Discord: %w", err) - } - return nil, nil -} - -// Creates a new instance of the DiscordRPPlugin, with all host services as dependencies -var plugin = &DiscordRPPlugin{ - cfg: config.NewConfigService(), - artwork: artwork.NewArtworkService(), - rpc: &discordRPC{ - ws: websocket.NewWebSocketService(), - web: http.NewHttpService(), - mem: cache.NewCacheService(), - }, -} - -func init() { - // Configure logging: No timestamps, no source file/line, prepend [Discord] - log.SetFlags(0) - log.SetPrefix("[Discord] ") - - // Register plugin capabilities - api.RegisterScrobbler(plugin) - api.RegisterWebSocketCallback(plugin.rpc) - - // Register named scheduler callbacks, and get the scheduler service for each - plugin.sched = api.RegisterNamedSchedulerCallback("close-activity", plugin) - plugin.rpc.sched = api.RegisterNamedSchedulerCallback("heartbeat", plugin.rpc) -} - -func main() {} diff --git a/plugins/examples/discord-rich-presence/rpc.go b/plugins/examples/discord-rich-presence/rpc.go deleted file mode 100644 index 4fab42f41..000000000 --- a/plugins/examples/discord-rich-presence/rpc.go +++ /dev/null @@ -1,402 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "strings" - "time" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/cache" - "github.com/navidrome/navidrome/plugins/host/http" - "github.com/navidrome/navidrome/plugins/host/scheduler" - "github.com/navidrome/navidrome/plugins/host/websocket" -) - -type discordRPC struct { - ws websocket.WebSocketService - web http.HttpService - mem cache.CacheService - sched scheduler.SchedulerService -} - -// Discord WebSocket Gateway constants -const ( - heartbeatOpCode = 1 // Heartbeat operation code - gateOpCode = 2 // Identify operation code - presenceOpCode = 3 // Presence update operation code -) - -const ( - heartbeatInterval = 41 // Heartbeat interval in seconds - defaultImage = "https://i.imgur.com/hb3XPzA.png" -) - -// Activity is a struct that represents an activity in Discord. -type activity struct { - Name string `json:"name"` - Type int `json:"type"` - Details string `json:"details"` - State string `json:"state"` - Application string `json:"application_id"` - Timestamps activityTimestamps `json:"timestamps"` - Assets activityAssets `json:"assets"` -} - -type activityTimestamps struct { - Start int64 `json:"start"` - End int64 `json:"end"` -} - -type activityAssets struct { - LargeImage string `json:"large_image"` - LargeText string `json:"large_text"` -} - -// PresencePayload is a struct that represents a presence update in Discord. -type presencePayload struct { - Activities []activity `json:"activities"` - Since int64 `json:"since"` - Status string `json:"status"` - Afk bool `json:"afk"` -} - -// IdentifyPayload is a struct that represents an identify payload in Discord. -type identifyPayload struct { - Token string `json:"token"` - Intents int `json:"intents"` - Properties identifyProperties `json:"properties"` -} - -type identifyProperties struct { - OS string `json:"os"` - Browser string `json:"browser"` - Device string `json:"device"` -} - -func (r *discordRPC) processImage(ctx context.Context, imageURL string, clientID string, token string) (string, error) { - return r.processImageWithFallback(ctx, imageURL, clientID, token, false) -} - -func (r *discordRPC) processImageWithFallback(ctx context.Context, imageURL string, clientID string, token string, isDefaultImage bool) (string, error) { - // Check if context is canceled - if err := ctx.Err(); err != nil { - return "", fmt.Errorf("context canceled: %w", err) - } - - if imageURL == "" { - if isDefaultImage { - // We're already processing the default image and it's empty, return error - return "", fmt.Errorf("default image URL is empty") - } - return r.processImageWithFallback(ctx, defaultImage, clientID, token, true) - } - - if strings.HasPrefix(imageURL, "mp:") { - return imageURL, nil - } - - // Check cache first - cacheKey := fmt.Sprintf("discord.image.%x", imageURL) - cacheResp, _ := r.mem.GetString(ctx, &cache.GetRequest{Key: cacheKey}) - if cacheResp.Exists { - log.Printf("Cache hit for image URL: %s", imageURL) - return cacheResp.Value, nil - } - - resp, _ := r.web.Post(ctx, &http.HttpRequest{ - Url: fmt.Sprintf("https://discord.com/api/v9/applications/%s/external-assets", clientID), - Headers: map[string]string{ - "Authorization": token, - "Content-Type": "application/json", - }, - Body: fmt.Appendf(nil, `{"urls":[%q]}`, imageURL), - }) - - // Handle HTTP error responses - if resp.Status >= 400 { - if isDefaultImage { - return "", fmt.Errorf("failed to process default image: HTTP %d %s", resp.Status, resp.Error) - } - return r.processImageWithFallback(ctx, defaultImage, clientID, token, true) - } - if resp.Error != "" { - if isDefaultImage { - // If we're already processing the default image and it fails, return error - return "", fmt.Errorf("failed to process default image: %s", resp.Error) - } - // Try with default image - return r.processImageWithFallback(ctx, defaultImage, clientID, token, true) - } - - var data []map[string]string - if err := json.Unmarshal(resp.Body, &data); err != nil { - if isDefaultImage { - // If we're already processing the default image and it fails, return error - return "", fmt.Errorf("failed to unmarshal default image response: %w", err) - } - // Try with default image - return r.processImageWithFallback(ctx, defaultImage, clientID, token, true) - } - - if len(data) == 0 { - if isDefaultImage { - // If we're already processing the default image and it fails, return error - return "", fmt.Errorf("no data returned for default image") - } - // Try with default image - return r.processImageWithFallback(ctx, defaultImage, clientID, token, true) - } - - image := data[0]["external_asset_path"] - if image == "" { - if isDefaultImage { - // If we're already processing the default image and it fails, return error - return "", fmt.Errorf("empty external_asset_path for default image") - } - // Try with default image - return r.processImageWithFallback(ctx, defaultImage, clientID, token, true) - } - - processedImage := fmt.Sprintf("mp:%s", image) - - // Cache the processed image URL - var ttl = 4 * time.Hour // 4 hours for regular images - if isDefaultImage { - ttl = 48 * time.Hour // 48 hours for default image - } - - _, _ = r.mem.SetString(ctx, &cache.SetStringRequest{ - Key: cacheKey, - Value: processedImage, - TtlSeconds: int64(ttl.Seconds()), - }) - - log.Printf("Cached processed image URL for %s (TTL: %s seconds)", imageURL, ttl) - - return processedImage, nil -} - -func (r *discordRPC) sendActivity(ctx context.Context, clientID, username, token string, data activity) error { - log.Printf("Sending activity to for user %s: %#v", username, data) - - processedImage, err := r.processImage(ctx, data.Assets.LargeImage, clientID, token) - if err != nil { - log.Printf("Failed to process image for user %s, continuing without image: %v", username, err) - // Clear the image and continue without it - data.Assets.LargeImage = "" - } else { - log.Printf("Processed image for URL %s: %s", data.Assets.LargeImage, processedImage) - data.Assets.LargeImage = processedImage - } - - presence := presencePayload{ - Activities: []activity{data}, - Status: "dnd", - Afk: false, - } - return r.sendMessage(ctx, username, presenceOpCode, presence) -} - -func (r *discordRPC) clearActivity(ctx context.Context, username string) error { - log.Printf("Clearing activity for user %s", username) - return r.sendMessage(ctx, username, presenceOpCode, presencePayload{}) -} - -func (r *discordRPC) sendMessage(ctx context.Context, username string, opCode int, payload any) error { - message := map[string]any{ - "op": opCode, - "d": payload, - } - b, err := json.Marshal(message) - if err != nil { - return fmt.Errorf("failed to marshal presence update: %w", err) - } - - resp, _ := r.ws.SendText(ctx, &websocket.SendTextRequest{ - ConnectionId: username, - Message: string(b), - }) - if resp.Error != "" { - return fmt.Errorf("failed to send presence update: %s", resp.Error) - } - return nil -} - -func (r *discordRPC) getDiscordGateway(ctx context.Context) (string, error) { - resp, _ := r.web.Get(ctx, &http.HttpRequest{ - Url: "https://discord.com/api/gateway", - }) - if resp.Error != "" { - return "", fmt.Errorf("failed to get Discord gateway: %s", resp.Error) - } - var result map[string]string - err := json.Unmarshal(resp.Body, &result) - if err != nil { - return "", fmt.Errorf("failed to parse Discord gateway response: %w", err) - } - return result["url"], nil -} - -func (r *discordRPC) sendHeartbeat(ctx context.Context, username string) error { - resp, _ := r.mem.GetInt(ctx, &cache.GetRequest{ - Key: fmt.Sprintf("discord.seq.%s", username), - }) - log.Printf("Sending heartbeat for user %s: %d", username, resp.Value) - return r.sendMessage(ctx, username, heartbeatOpCode, resp.Value) -} - -func (r *discordRPC) cleanupFailedConnection(ctx context.Context, username string) { - log.Printf("Cleaning up failed connection for user %s", username) - - // Cancel the heartbeat schedule - if resp, _ := r.sched.CancelSchedule(ctx, &scheduler.CancelRequest{ScheduleId: username}); resp.Error != "" { - log.Printf("Failed to cancel heartbeat schedule for user %s: %s", username, resp.Error) - } - - // Close the WebSocket connection - if resp, _ := r.ws.Close(ctx, &websocket.CloseRequest{ - ConnectionId: username, - Code: 1000, - Reason: "Connection lost", - }); resp.Error != "" { - log.Printf("Failed to close WebSocket connection for user %s: %s", username, resp.Error) - } - - // Clean up cache entries (just the sequence number, no failure tracking needed) - _, _ = r.mem.Remove(ctx, &cache.RemoveRequest{Key: fmt.Sprintf("discord.seq.%s", username)}) - - log.Printf("Cleaned up connection for user %s", username) -} - -func (r *discordRPC) isConnected(ctx context.Context, username string) bool { - // Try to send a heartbeat to test the connection - err := r.sendHeartbeat(ctx, username) - if err != nil { - log.Printf("Heartbeat test failed for user %s: %v", username, err) - return false - } - return true -} - -func (r *discordRPC) connect(ctx context.Context, username string, token string) error { - if r.isConnected(ctx, username) { - log.Printf("Reusing existing connection for user %s", username) - return nil - } - log.Printf("Creating new connection for user %s", username) - - // Get Discord Gateway URL - gateway, err := r.getDiscordGateway(ctx) - if err != nil { - return fmt.Errorf("failed to get Discord gateway: %w", err) - } - log.Printf("Using gateway: %s", gateway) - - // Connect to Discord Gateway - resp, _ := r.ws.Connect(ctx, &websocket.ConnectRequest{ - ConnectionId: username, - Url: gateway, - }) - if resp.Error != "" { - return fmt.Errorf("failed to connect to WebSocket: %s", resp.Error) - } - - // Send identify payload - payload := identifyPayload{ - Token: token, - Intents: 0, - Properties: identifyProperties{ - OS: "Windows 10", - Browser: "Discord Client", - Device: "Discord Client", - }, - } - err = r.sendMessage(ctx, username, gateOpCode, payload) - if err != nil { - return fmt.Errorf("failed to send identify payload: %w", err) - } - - // Schedule heartbeats for this user/connection - cronResp, _ := r.sched.ScheduleRecurring(ctx, &scheduler.ScheduleRecurringRequest{ - CronExpression: fmt.Sprintf("@every %ds", heartbeatInterval), - ScheduleId: username, - }) - log.Printf("Scheduled heartbeat for user %s with ID %s", username, cronResp.ScheduleId) - - log.Printf("Successfully authenticated user %s", username) - return nil -} - -func (r *discordRPC) disconnect(ctx context.Context, username string) error { - if resp, _ := r.sched.CancelSchedule(ctx, &scheduler.CancelRequest{ScheduleId: username}); resp.Error != "" { - return fmt.Errorf("failed to cancel schedule: %s", resp.Error) - } - resp, _ := r.ws.Close(ctx, &websocket.CloseRequest{ - ConnectionId: username, - Code: 1000, - Reason: "Navidrome disconnect", - }) - if resp.Error != "" { - return fmt.Errorf("failed to close WebSocket connection: %s", resp.Error) - } - return nil -} - -func (r *discordRPC) OnTextMessage(ctx context.Context, req *api.OnTextMessageRequest) (*api.OnTextMessageResponse, error) { - if len(req.Message) < 1024 { - log.Printf("Received WebSocket message for connection '%s': %s", req.ConnectionId, req.Message) - } else { - log.Printf("Received WebSocket message for connection '%s' (truncated): %s...", req.ConnectionId, req.Message[:1021]) - } - - // Parse the message. If it's a heartbeat_ack, store the sequence number. - message := map[string]any{} - err := json.Unmarshal([]byte(req.Message), &message) - if err != nil { - return nil, fmt.Errorf("failed to parse WebSocket message: %w", err) - } - if v := message["s"]; v != nil { - seq := int64(v.(float64)) - log.Printf("Received heartbeat_ack for connection '%s': %d", req.ConnectionId, seq) - resp, _ := r.mem.SetInt(ctx, &cache.SetIntRequest{ - Key: fmt.Sprintf("discord.seq.%s", req.ConnectionId), - Value: seq, - TtlSeconds: heartbeatInterval * 2, - }) - if !resp.Success { - return nil, fmt.Errorf("failed to store sequence number for user %s", req.ConnectionId) - } - } - return nil, nil -} - -func (r *discordRPC) OnBinaryMessage(_ context.Context, req *api.OnBinaryMessageRequest) (*api.OnBinaryMessageResponse, error) { - log.Printf("Received unexpected binary message for connection '%s'", req.ConnectionId) - return nil, nil -} - -func (r *discordRPC) OnError(_ context.Context, req *api.OnErrorRequest) (*api.OnErrorResponse, error) { - log.Printf("WebSocket error for connection '%s': %s", req.ConnectionId, req.Error) - return nil, nil -} - -func (r *discordRPC) OnClose(_ context.Context, req *api.OnCloseRequest) (*api.OnCloseResponse, error) { - log.Printf("WebSocket connection '%s' closed with code %d: %s", req.ConnectionId, req.Code, req.Reason) - return nil, nil -} - -func (r *discordRPC) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) { - err := r.sendHeartbeat(ctx, req.ScheduleId) - if err != nil { - // On first heartbeat failure, immediately clean up the connection - // The next NowPlaying call will reconnect if needed - log.Printf("Heartbeat failed for user %s, cleaning up connection: %v", req.ScheduleId, err) - r.cleanupFailedConnection(ctx, req.ScheduleId) - return nil, fmt.Errorf("heartbeat failed, connection cleaned up: %w", err) - } - - return nil, nil -} diff --git a/plugins/examples/subsonicapi-demo/README.md b/plugins/examples/subsonicapi-demo/README.md deleted file mode 100644 index b5ac9f784..000000000 --- a/plugins/examples/subsonicapi-demo/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# SubsonicAPI Demo Plugin - -This example plugin demonstrates how to use the SubsonicAPI host service to access Navidrome's Subsonic API from within a plugin. - -## What it does - -The plugin performs the following operations during initialization: - -1. **Ping the server**: Calls `/rest/ping` to check if the Subsonic API is responding -2. **Get license info**: Calls `/rest/getLicense` to retrieve server license information - -## Key Features - -- Shows how to request `subsonicapi` permission in the manifest -- Demonstrates making Subsonic API calls using the `subsonicapi.Call()` method -- Handles both successful responses and errors -- Uses proper lifecycle management with `OnInit` - -## Usage - -### Manifest Configuration - -```json -{ - "permissions": { - "subsonicapi": { - "reason": "Demonstrate accessing Navidrome's Subsonic API from within plugins", - "allowAdmins": true - } - } -} -``` - -### Plugin Implementation - -```go -import "github.com/navidrome/navidrome/plugins/host/subsonicapi" - -var subsonicService = subsonicapi.NewSubsonicAPIService() - -// OnInit is called when the plugin is loaded -func (SubsonicAPIDemoPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { - // Make API calls - response, err := subsonicService.Call(ctx, &subsonicapi.CallRequest{ - Url: "/rest/ping?u=admin", - }) - // Handle response... -} -``` - -When running Navidrome with this plugin installed, it will automatically call the Subsonic API endpoints during the -server startup, and you can see the results in the logs: - -```agsl -INFO[0000] 2022/01/01 00:00:00 SubsonicAPI Demo Plugin initializing... -DEBU[0000] API: New request /ping client=subsonicapi-demo username=admin version=1.16.1 -DEBU[0000] API: Successful response endpoint=/ping status=OK -DEBU[0000] API: New request /getLicense client=subsonicapi-demo username=admin version=1.16.1 -INFO[0000] 2022/01/01 00:00:00 SubsonicAPI ping response: {"subsonic-response":{"status":"ok","version":"1.16.1","type":"navidrome","serverVersion":"dev","openSubsonic":true}} -DEBU[0000] API: Successful response endpoint=/getLicense status=OK -DEBU[0000] Plugin initialized successfully elapsed=41.9ms plugin=subsonicapi-demo -INFO[0000] 2022/01/01 00:00:00 SubsonicAPI license info: {"subsonic-response":{"status":"ok","version":"1.16.1","type":"navidrome","serverVersion":"dev","openSubsonic":true,"license":{"valid":true}}} -``` - -## Important Notes - -1. **Authentication**: The plugin must provide valid authentication parameters in the URL: - - **Required**: `u` (username) - The service validates this parameter is present - - Example: `"/rest/ping?u=admin"` -2. **URL Format**: Only the path and query parameters from the URL are used - host, protocol, and method are ignored -3. **Automatic Parameters**: The service automatically adds: - - `c`: Plugin name (client identifier) - - `v`: Subsonic API version (1.16.1) - - `f`: Response format (json) -4. **Internal Authentication**: The service sets up internal authentication using the `u` parameter -5. **Lifecycle**: This plugin uses `LifecycleManagement` with only the `OnInit` method - -## Building - -This plugin uses the `wasip1` build constraint and must be compiled for WebAssembly: - -```bash -# Using the project's make target (recommended) -make plugin-examples - -# Manual compilation (when using the proper toolchain) -GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm plugin.go -``` diff --git a/plugins/examples/subsonicapi-demo/manifest.json b/plugins/examples/subsonicapi-demo/manifest.json deleted file mode 100644 index d26c33181..000000000 --- a/plugins/examples/subsonicapi-demo/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json", - "name": "subsonicapi-demo", - "author": "Navidrome Team", - "version": "1.0.0", - "description": "Example plugin demonstrating SubsonicAPI host service usage", - "website": "https://github.com/navidrome/navidrome", - "capabilities": ["LifecycleManagement"], - "permissions": { - "subsonicapi": { - "reason": "Demonstrate accessing Navidrome's Subsonic API from within plugins", - "allowAdmins": true, - "allowedUsernames": ["admin"] - } - } -} diff --git a/plugins/examples/subsonicapi-demo/plugin.go b/plugins/examples/subsonicapi-demo/plugin.go deleted file mode 100644 index 4ca087ac7..000000000 --- a/plugins/examples/subsonicapi-demo/plugin.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "log" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/subsonicapi" -) - -// SubsonicAPIService instance for making API calls -var subsonicService = subsonicapi.NewSubsonicAPIService() - -// SubsonicAPIDemoPlugin implements LifecycleManagement interface -type SubsonicAPIDemoPlugin struct{} - -// OnInit is called when the plugin is loaded -func (SubsonicAPIDemoPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { - log.Printf("SubsonicAPI Demo Plugin initializing...") - - // Example: Call the ping endpoint to check if the server is alive - response, err := subsonicService.Call(ctx, &subsonicapi.CallRequest{ - Url: "/rest/ping?u=admin", - }) - - if err != nil { - log.Printf("SubsonicAPI call failed: %v", err) - return &api.InitResponse{Error: err.Error()}, nil - } - - if response.Error != "" { - log.Printf("SubsonicAPI returned error: %s", response.Error) - return &api.InitResponse{Error: response.Error}, nil - } - - log.Printf("SubsonicAPI ping response: %s", response.Json) - - // Example: Get server info - infoResponse, err := subsonicService.Call(ctx, &subsonicapi.CallRequest{ - Url: "/rest/getLicense?u=admin", - }) - - if err != nil { - log.Printf("SubsonicAPI getLicense call failed: %v", err) - return &api.InitResponse{Error: err.Error()}, nil - } - - if infoResponse.Error != "" { - log.Printf("SubsonicAPI getLicense returned error: %s", infoResponse.Error) - return &api.InitResponse{Error: infoResponse.Error}, nil - } - - log.Printf("SubsonicAPI license info: %s", infoResponse.Json) - - return &api.InitResponse{}, nil -} - -func main() {} - -func init() { - // Configure logging: No timestamps, no source file/line - log.SetFlags(0) - log.SetPrefix("[Subsonic Plugin] ") - - api.RegisterLifecycleManagement(&SubsonicAPIDemoPlugin{}) -} diff --git a/plugins/examples/wikimedia/README.md b/plugins/examples/wikimedia/README.md deleted file mode 100644 index 15feed2d3..000000000 --- a/plugins/examples/wikimedia/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Wikimedia Artist Metadata Plugin - -This is a WASM plugin for Navidrome that retrieves artist information from Wikidata/DBpedia using the Wikidata SPARQL endpoint. - -## Implemented Methods - -- `GetArtistBiography`: Returns the artist's English biography/description from Wikidata. -- `GetArtistURL`: Returns the artist's official website (if available) from Wikidata. -- `GetArtistImages`: Returns the artist's main image (Wikimedia Commons) from Wikidata. - -All other methods (`GetArtistMBID`, `GetSimilarArtists`, `GetArtistTopSongs`) return a "not implemented" error, as this data is not available from Wikidata/DBpedia. - -## How it Works - -- The plugin uses the host-provided HTTP service (`HttpService`) to make SPARQL queries to the Wikidata endpoint. -- No network requests are made directly from the plugin; all HTTP is routed through the host. - -## Building - -To build the plugin to WASM: - -``` -GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm plugin.go -``` - -## Usage - -Copy the resulting `plugin.wasm` to your Navidrome plugins folder under a `wikimedia` directory. - ---- - -For more details, see the source code in `plugin.go`. diff --git a/plugins/examples/wikimedia/manifest.json b/plugins/examples/wikimedia/manifest.json deleted file mode 100644 index 5d0196e0a..000000000 --- a/plugins/examples/wikimedia/manifest.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json", - "name": "wikimedia", - "author": "Navidrome", - "version": "1.0.0", - "description": "Artist information and images from Wikimedia Commons", - "website": "https://commons.wikimedia.org", - "capabilities": ["MetadataAgent"], - "permissions": { - "http": { - "reason": "To fetch artist information and images from Wikimedia Commons API", - "allowedUrls": { - "https://*.wikimedia.org": ["GET"], - "https://*.wikipedia.org": ["GET"], - "https://commons.wikimedia.org": ["GET"] - }, - "allowLocalNetwork": false - } - } -} diff --git a/plugins/examples/wikimedia/plugin.go b/plugins/examples/wikimedia/plugin.go deleted file mode 100644 index 6b60e69da..000000000 --- a/plugins/examples/wikimedia/plugin.go +++ /dev/null @@ -1,391 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "log" - "net/url" - "strings" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/http" -) - -const ( - wikidataEndpoint = "https://query.wikidata.org/sparql" - dbpediaEndpoint = "https://dbpedia.org/sparql" - mediawikiAPIEndpoint = "https://en.wikipedia.org/w/api.php" - requestTimeoutMs = 5000 -) - -var ( - ErrNotFound = api.ErrNotFound - ErrNotImplemented = api.ErrNotImplemented - - client = http.NewHttpService() -) - -// SPARQLResult struct for all possible fields -// Only the needed field will be non-nil in each context -// (Sitelink, Wiki, Comment, Img) -type SPARQLResult struct { - Results struct { - Bindings []struct { - Sitelink *struct{ Value string } `json:"sitelink,omitempty"` - Wiki *struct{ Value string } `json:"wiki,omitempty"` - Comment *struct{ Value string } `json:"comment,omitempty"` - Img *struct{ Value string } `json:"img,omitempty"` - } `json:"bindings"` - } `json:"results"` -} - -// MediaWikiExtractResult is used to unmarshal MediaWiki API extract responses -// (for getWikipediaExtract) -type MediaWikiExtractResult struct { - Query struct { - Pages map[string]struct { - PageID int `json:"pageid"` - Ns int `json:"ns"` - Title string `json:"title"` - Extract string `json:"extract"` - Missing bool `json:"missing"` - } `json:"pages"` - } `json:"query"` -} - -// --- SPARQL Query Helper --- -func sparqlQuery(ctx context.Context, client http.HttpService, endpoint, query string) (*SPARQLResult, error) { - form := url.Values{} - form.Set("query", query) - - req := &http.HttpRequest{ - Url: endpoint, - Headers: map[string]string{ - "Accept": "application/sparql-results+json", - "Content-Type": "application/x-www-form-urlencoded", // Required by SPARQL endpoints - "User-Agent": "NavidromeWikimediaPlugin/0.1", - }, - Body: []byte(form.Encode()), // Send encoded form data - TimeoutMs: requestTimeoutMs, - } - log.Printf("[Wikimedia Query] Attempting SPARQL query to %s (query length: %d):\n%s", endpoint, len(query), query) - resp, err := client.Post(ctx, req) - if err != nil { - return nil, fmt.Errorf("SPARQL request error: %w", err) - } - if resp.Status != 200 { - log.Printf("[Wikimedia Query] SPARQL HTTP error %d for query to %s. Body: %s", resp.Status, endpoint, string(resp.Body)) - return nil, fmt.Errorf("SPARQL HTTP error: status %d", resp.Status) - } - var result SPARQLResult - if err := json.Unmarshal(resp.Body, &result); err != nil { - return nil, fmt.Errorf("failed to parse SPARQL response: %w", err) - } - if len(result.Results.Bindings) == 0 { - return nil, ErrNotFound - } - return &result, nil -} - -// --- MediaWiki API Helper --- -func mediawikiQuery(ctx context.Context, client http.HttpService, params url.Values) ([]byte, error) { - apiURL := fmt.Sprintf("%s?%s", mediawikiAPIEndpoint, params.Encode()) - req := &http.HttpRequest{ - Url: apiURL, - Headers: map[string]string{ - "Accept": "application/json", - "User-Agent": "NavidromeWikimediaPlugin/0.1", - }, - TimeoutMs: requestTimeoutMs, - } - resp, err := client.Get(ctx, req) - if err != nil { - return nil, fmt.Errorf("MediaWiki request error: %w", err) - } - if resp.Status != 200 { - return nil, fmt.Errorf("MediaWiki HTTP error: status %d, body: %s", resp.Status, string(resp.Body)) - } - return resp.Body, nil -} - -// --- Wikidata Fetch Functions --- -func getWikidataWikipediaURL(ctx context.Context, client http.HttpService, mbid, name string) (string, error) { - var q string - if mbid != "" { - // Using property chain: ?sitelink schema:about ?artist; schema:isPartOf . - q = fmt.Sprintf(`SELECT ?sitelink WHERE { ?artist wdt:P434 "%s". ?sitelink schema:about ?artist; schema:isPartOf . } LIMIT 1`, mbid) - } else if name != "" { - escapedName := strings.ReplaceAll(name, "\"", "\\\"") - // Using property chain: ?sitelink schema:about ?artist; schema:isPartOf . - q = fmt.Sprintf(`SELECT ?sitelink WHERE { ?artist rdfs:label "%s"@en. ?sitelink schema:about ?artist; schema:isPartOf . } LIMIT 1`, escapedName) - } else { - return "", errors.New("MBID or Name required for Wikidata URL lookup") - } - - result, err := sparqlQuery(ctx, client, wikidataEndpoint, q) - if err != nil { - return "", fmt.Errorf("Wikidata SPARQL query failed: %w", err) - } - if result.Results.Bindings[0].Sitelink != nil { - return result.Results.Bindings[0].Sitelink.Value, nil - } - return "", ErrNotFound -} - -// --- DBpedia Fetch Functions --- -func getDBpediaWikipediaURL(ctx context.Context, client http.HttpService, name string) (string, error) { - if name == "" { - return "", ErrNotFound - } - escapedName := strings.ReplaceAll(name, "\"", "\\\"") - q := fmt.Sprintf(`SELECT ?wiki WHERE { ?artist foaf:name "%s"@en; foaf:isPrimaryTopicOf ?wiki. FILTER regex(str(?wiki), "^https://en.wikipedia.org/") } LIMIT 1`, escapedName) - result, err := sparqlQuery(ctx, client, dbpediaEndpoint, q) - if err != nil { - return "", fmt.Errorf("DBpedia SPARQL query failed: %w", err) - } - if result.Results.Bindings[0].Wiki != nil { - return result.Results.Bindings[0].Wiki.Value, nil - } - return "", ErrNotFound -} - -func getDBpediaComment(ctx context.Context, client http.HttpService, name string) (string, error) { - if name == "" { - return "", ErrNotFound - } - escapedName := strings.ReplaceAll(name, "\"", "\\\"") - q := fmt.Sprintf(`SELECT ?comment WHERE { ?artist foaf:name "%s"@en; rdfs:comment ?comment. FILTER (lang(?comment) = 'en') } LIMIT 1`, escapedName) - result, err := sparqlQuery(ctx, client, dbpediaEndpoint, q) - if err != nil { - return "", fmt.Errorf("DBpedia comment SPARQL query failed: %w", err) - } - if result.Results.Bindings[0].Comment != nil { - return result.Results.Bindings[0].Comment.Value, nil - } - return "", ErrNotFound -} - -// --- Wikipedia API Fetch Function --- -func getWikipediaExtract(ctx context.Context, client http.HttpService, pageTitle string) (string, error) { - if pageTitle == "" { - return "", errors.New("page title required for Wikipedia API lookup") - } - params := url.Values{} - params.Set("action", "query") - params.Set("format", "json") - params.Set("prop", "extracts") - params.Set("exintro", "true") // Intro section only - params.Set("explaintext", "true") // Plain text - params.Set("titles", pageTitle) - params.Set("redirects", "1") // Follow redirects - - body, err := mediawikiQuery(ctx, client, params) - if err != nil { - return "", fmt.Errorf("MediaWiki query failed: %w", err) - } - - var result MediaWikiExtractResult - if err := json.Unmarshal(body, &result); err != nil { - return "", fmt.Errorf("failed to parse MediaWiki response: %w", err) - } - - // Iterate through the pages map (usually only one page) - for _, page := range result.Query.Pages { - if page.Missing { - continue // Skip missing pages - } - if page.Extract != "" { - return strings.TrimSpace(page.Extract), nil - } - } - - return "", ErrNotFound -} - -// --- Helper to get Wikipedia Page Title from URL --- -func extractPageTitleFromURL(wikiURL string) (string, error) { - parsedURL, err := url.Parse(wikiURL) - if err != nil { - return "", err - } - if parsedURL.Host != "en.wikipedia.org" { - return "", fmt.Errorf("URL host is not en.wikipedia.org: %s", parsedURL.Host) - } - pathParts := strings.Split(strings.TrimPrefix(parsedURL.Path, "/"), "/") - if len(pathParts) < 2 || pathParts[0] != "wiki" { - return "", fmt.Errorf("URL path does not match /wiki/ format: %s", parsedURL.Path) - } - title := pathParts[1] - if title == "" { - return "", errors.New("extracted title is empty") - } - decodedTitle, err := url.PathUnescape(title) - if err != nil { - return "", fmt.Errorf("failed to decode title '%s': %w", title, err) - } - return decodedTitle, nil -} - -// --- Agent Implementation --- -type WikimediaAgent struct{} - -// GetArtistURL fetches the Wikipedia URL. -// Order: Wikidata(MBID/Name) -> DBpedia(Name) -> Search URL -func (WikimediaAgent) GetArtistURL(ctx context.Context, req *api.ArtistURLRequest) (*api.ArtistURLResponse, error) { - var wikiURL string - var err error - - // 1. Try Wikidata (MBID first, then name) - wikiURL, err = getWikidataWikipediaURL(ctx, client, req.Mbid, req.Name) - if err == nil && wikiURL != "" { - return &api.ArtistURLResponse{Url: wikiURL}, nil - } - if err != nil && err != ErrNotFound { - log.Printf("[Wikimedia] Error fetching Wikidata URL: %v\n", err) - // Don't stop, try DBpedia - } - - // 2. Try DBpedia (Name only) - if req.Name != "" { - wikiURL, err = getDBpediaWikipediaURL(ctx, client, req.Name) - if err == nil && wikiURL != "" { - return &api.ArtistURLResponse{Url: wikiURL}, nil - } - if err != nil && err != ErrNotFound { - log.Printf("[Wikimedia] Error fetching DBpedia URL: %v\n", err) - // Don't stop, generate search URL - } - } - - // 3. Fallback to search URL - if req.Name != "" { - searchURL := fmt.Sprintf("https://en.wikipedia.org/w/index.php?search=%s", url.QueryEscape(req.Name)) - log.Printf("[Wikimedia] URL not found, falling back to search URL: %s\n", searchURL) - return &api.ArtistURLResponse{Url: searchURL}, nil - } - - log.Printf("[Wikimedia] Could not determine Wikipedia URL for: %s (%s)\n", req.Name, req.Mbid) - return nil, ErrNotFound -} - -// GetArtistBiography fetches the long biography. -// Order: Wikipedia API (via Wikidata/DBpedia URL) -> DBpedia Comment (Name) -func (WikimediaAgent) GetArtistBiography(ctx context.Context, req *api.ArtistBiographyRequest) (*api.ArtistBiographyResponse, error) { - var bio string - var err error - - log.Printf("[Wikimedia Bio] Fetching for Name: %s, MBID: %s", req.Name, req.Mbid) - - // 1. Get Wikipedia URL (using the logic from GetArtistURL) - wikiURL := "" - // Try Wikidata first - tempURL, wdErr := getWikidataWikipediaURL(ctx, client, req.Mbid, req.Name) - if wdErr == nil && tempURL != "" { - log.Printf("[Wikimedia Bio] Found Wikidata URL: %s", tempURL) - wikiURL = tempURL - } else if req.Name != "" { - // Try DBpedia if Wikidata failed or returned not found - log.Printf("[Wikimedia Bio] Wikidata URL failed (%v), trying DBpedia URL", wdErr) - tempURL, dbErr := getDBpediaWikipediaURL(ctx, client, req.Name) - if dbErr == nil && tempURL != "" { - log.Printf("[Wikimedia Bio] Found DBpedia URL: %s", tempURL) - wikiURL = tempURL - } else { - log.Printf("[Wikimedia Bio] DBpedia URL failed (%v)", dbErr) - } - } - - // 2. If Wikipedia URL found, try MediaWiki API - if wikiURL != "" { - pageTitle, err := extractPageTitleFromURL(wikiURL) - if err == nil { - log.Printf("[Wikimedia Bio] Extracted page title: %s", pageTitle) - bio, err = getWikipediaExtract(ctx, client, pageTitle) - if err == nil && bio != "" { - log.Printf("[Wikimedia Bio] Found Wikipedia extract.") - return &api.ArtistBiographyResponse{Biography: bio}, nil - } - log.Printf("[Wikimedia Bio] Wikipedia extract failed: %v", err) - if err != nil && err != ErrNotFound { - log.Printf("[Wikimedia Bio] Error fetching Wikipedia extract for '%s': %v", pageTitle, err) - // Don't stop, try DBpedia comment - } - } else { - log.Printf("[Wikimedia Bio] Error extracting page title from URL '%s': %v", wikiURL, err) - // Don't stop, try DBpedia comment - } - } - - // 3. Fallback to DBpedia Comment (Name only) - if req.Name != "" { - log.Printf("[Wikimedia Bio] Falling back to DBpedia comment for name: %s", req.Name) - bio, err = getDBpediaComment(ctx, client, req.Name) - if err == nil && bio != "" { - log.Printf("[Wikimedia Bio] Found DBpedia comment.") - return &api.ArtistBiographyResponse{Biography: bio}, nil - } - log.Printf("[Wikimedia Bio] DBpedia comment failed: %v", err) - if err != nil && err != ErrNotFound { - log.Printf("[Wikimedia Bio] Error fetching DBpedia comment for '%s': %v", req.Name, err) - } - } - - log.Printf("[Wikimedia Bio] Final: Biography not found for: %s (%s)", req.Name, req.Mbid) - return nil, ErrNotFound -} - -// GetArtistImages fetches images (Wikidata only for now) -func (WikimediaAgent) GetArtistImages(ctx context.Context, req *api.ArtistImageRequest) (*api.ArtistImageResponse, error) { - var q string - if req.Mbid != "" { - q = fmt.Sprintf(`SELECT ?img WHERE { ?artist wdt:P434 "%s"; wdt:P18 ?img } LIMIT 1`, req.Mbid) - } else if req.Name != "" { - escapedName := strings.ReplaceAll(req.Name, "\"", "\\\"") - q = fmt.Sprintf(`SELECT ?img WHERE { ?artist rdfs:label "%s"@en; wdt:P18 ?img } LIMIT 1`, escapedName) - } else { - return nil, errors.New("MBID or Name required for Wikidata Image lookup") - } - - result, err := sparqlQuery(ctx, client, wikidataEndpoint, q) - if err != nil { - log.Printf("[Wikimedia] Image not found for: %s (%s)\n", req.Name, req.Mbid) - return nil, ErrNotFound - } - if result.Results.Bindings[0].Img != nil { - return &api.ArtistImageResponse{Images: []*api.ExternalImage{{Url: result.Results.Bindings[0].Img.Value, Size: 0}}}, nil - } - log.Printf("[Wikimedia] Image not found for: %s (%s)\n", req.Name, req.Mbid) - return nil, ErrNotFound -} - -// Not implemented methods -func (WikimediaAgent) GetArtistMBID(context.Context, *api.ArtistMBIDRequest) (*api.ArtistMBIDResponse, error) { - return nil, ErrNotImplemented -} -func (WikimediaAgent) GetSimilarArtists(context.Context, *api.ArtistSimilarRequest) (*api.ArtistSimilarResponse, error) { - return nil, ErrNotImplemented -} -func (WikimediaAgent) GetArtistTopSongs(context.Context, *api.ArtistTopSongsRequest) (*api.ArtistTopSongsResponse, error) { - return nil, ErrNotImplemented -} -func (WikimediaAgent) GetAlbumInfo(context.Context, *api.AlbumInfoRequest) (*api.AlbumInfoResponse, error) { - return nil, ErrNotImplemented -} - -func (WikimediaAgent) GetAlbumImages(context.Context, *api.AlbumImagesRequest) (*api.AlbumImagesResponse, error) { - return nil, ErrNotImplemented -} - -func main() {} - -func init() { - // Configure logging: No timestamps, no source file/line - log.SetFlags(0) - log.SetPrefix("[Wikimedia] ") - - api.RegisterMetadataAgent(WikimediaAgent{}) -} diff --git a/plugins/host/artwork/artwork.pb.go b/plugins/host/artwork/artwork.pb.go deleted file mode 100644 index 228eced22..000000000 --- a/plugins/host/artwork/artwork.pb.go +++ /dev/null @@ -1,73 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/artwork/artwork.proto - -package artwork - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetArtworkUrlRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` // Optional, 0 means original size -} - -func (x *GetArtworkUrlRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetArtworkUrlRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *GetArtworkUrlRequest) GetSize() int32 { - if x != nil { - return x.Size - } - return 0 -} - -type GetArtworkUrlResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` -} - -func (x *GetArtworkUrlResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetArtworkUrlResponse) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -// go:plugin type=host version=1 -type ArtworkService interface { - GetArtistUrl(context.Context, *GetArtworkUrlRequest) (*GetArtworkUrlResponse, error) - GetAlbumUrl(context.Context, *GetArtworkUrlRequest) (*GetArtworkUrlResponse, error) - GetTrackUrl(context.Context, *GetArtworkUrlRequest) (*GetArtworkUrlResponse, error) -} diff --git a/plugins/host/artwork/artwork.proto b/plugins/host/artwork/artwork.proto deleted file mode 100644 index cb562e536..000000000 --- a/plugins/host/artwork/artwork.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto3"; - -package artwork; - -option go_package = "github.com/navidrome/navidrome/plugins/host/artwork;artwork"; - -// go:plugin type=host version=1 -service ArtworkService { - rpc GetArtistUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse); - rpc GetAlbumUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse); - rpc GetTrackUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse); -} - -message GetArtworkUrlRequest { - string id = 1; - int32 size = 2; // Optional, 0 means original size -} - -message GetArtworkUrlResponse { - string url = 1; -} \ No newline at end of file diff --git a/plugins/host/artwork/artwork_host.pb.go b/plugins/host/artwork/artwork_host.pb.go deleted file mode 100644 index 346fe1449..000000000 --- a/plugins/host/artwork/artwork_host.pb.go +++ /dev/null @@ -1,130 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/artwork/artwork.proto - -package artwork - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _artworkService struct { - ArtworkService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions ArtworkService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _artworkService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetArtistUrl), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_artist_url") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetAlbumUrl), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_album_url") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetTrackUrl), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_track_url") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -func (h _artworkService) _GetArtistUrl(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetArtworkUrlRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetArtistUrl(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _artworkService) _GetAlbumUrl(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetArtworkUrlRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetAlbumUrl(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _artworkService) _GetTrackUrl(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetArtworkUrlRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetTrackUrl(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/artwork/artwork_plugin.pb.go b/plugins/host/artwork/artwork_plugin.pb.go deleted file mode 100644 index f54aac0b9..000000000 --- a/plugins/host/artwork/artwork_plugin.pb.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/artwork/artwork.proto - -package artwork - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type artworkService struct{} - -func NewArtworkService() ArtworkService { - return artworkService{} -} - -//go:wasmimport env get_artist_url -func _get_artist_url(ptr uint32, size uint32) uint64 - -func (h artworkService) GetArtistUrl(ctx context.Context, request *GetArtworkUrlRequest) (*GetArtworkUrlResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_artist_url(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetArtworkUrlResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env get_album_url -func _get_album_url(ptr uint32, size uint32) uint64 - -func (h artworkService) GetAlbumUrl(ctx context.Context, request *GetArtworkUrlRequest) (*GetArtworkUrlResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_album_url(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetArtworkUrlResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env get_track_url -func _get_track_url(ptr uint32, size uint32) uint64 - -func (h artworkService) GetTrackUrl(ctx context.Context, request *GetArtworkUrlRequest) (*GetArtworkUrlResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_track_url(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetArtworkUrlResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/artwork/artwork_plugin_dev.go b/plugins/host/artwork/artwork_plugin_dev.go deleted file mode 100644 index 0071f5726..000000000 --- a/plugins/host/artwork/artwork_plugin_dev.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !wasip1 - -package artwork - -func NewArtworkService() ArtworkService { - panic("not implemented") -} diff --git a/plugins/host/artwork/artwork_vtproto.pb.go b/plugins/host/artwork/artwork_vtproto.pb.go deleted file mode 100644 index 6a1c0ba4e..000000000 --- a/plugins/host/artwork/artwork_vtproto.pb.go +++ /dev/null @@ -1,425 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/artwork/artwork.proto - -package artwork - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *GetArtworkUrlRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetArtworkUrlRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetArtworkUrlRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Size != 0 { - i = encodeVarint(dAtA, i, uint64(m.Size)) - i-- - dAtA[i] = 0x10 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = encodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *GetArtworkUrlResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetArtworkUrlResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetArtworkUrlResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *GetArtworkUrlRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Size != 0 { - n += 1 + sov(uint64(m.Size)) - } - n += len(m.unknownFields) - return n -} - -func (m *GetArtworkUrlResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *GetArtworkUrlRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetArtworkUrlRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetArtworkUrlRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Id = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Size", wireType) - } - m.Size = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Size |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetArtworkUrlResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetArtworkUrlResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetArtworkUrlResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host/cache/cache.pb.go b/plugins/host/cache/cache.pb.go deleted file mode 100644 index 6113a89b4..000000000 --- a/plugins/host/cache/cache.pb.go +++ /dev/null @@ -1,420 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/cache/cache.proto - -package cache - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Request to store a string value -type SetStringRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // String value to store - TtlSeconds int64 `protobuf:"varint,3,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"` // TTL in seconds, 0 means use default -} - -func (x *SetStringRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SetStringRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *SetStringRequest) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -func (x *SetStringRequest) GetTtlSeconds() int64 { - if x != nil { - return x.TtlSeconds - } - return 0 -} - -// Request to store an integer value -type SetIntRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key - Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` // Integer value to store - TtlSeconds int64 `protobuf:"varint,3,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"` // TTL in seconds, 0 means use default -} - -func (x *SetIntRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SetIntRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *SetIntRequest) GetValue() int64 { - if x != nil { - return x.Value - } - return 0 -} - -func (x *SetIntRequest) GetTtlSeconds() int64 { - if x != nil { - return x.TtlSeconds - } - return 0 -} - -// Request to store a float value -type SetFloatRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key - Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Float value to store - TtlSeconds int64 `protobuf:"varint,3,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"` // TTL in seconds, 0 means use default -} - -func (x *SetFloatRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SetFloatRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *SetFloatRequest) GetValue() float64 { - if x != nil { - return x.Value - } - return 0 -} - -func (x *SetFloatRequest) GetTtlSeconds() int64 { - if x != nil { - return x.TtlSeconds - } - return 0 -} - -// Request to store a byte slice value -type SetBytesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // Byte slice value to store - TtlSeconds int64 `protobuf:"varint,3,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"` // TTL in seconds, 0 means use default -} - -func (x *SetBytesRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SetBytesRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *SetBytesRequest) GetValue() []byte { - if x != nil { - return x.Value - } - return nil -} - -func (x *SetBytesRequest) GetTtlSeconds() int64 { - if x != nil { - return x.TtlSeconds - } - return 0 -} - -// Response after setting a value -type SetResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` // Whether the operation was successful -} - -func (x *SetResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SetResponse) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -// Request to get a value -type GetRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key -} - -func (x *GetRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -// Response containing a string value -type GetStringResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Whether the key exists - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // The string value (if exists is true) -} - -func (x *GetStringResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetStringResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -func (x *GetStringResponse) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -// Response containing an integer value -type GetIntResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Whether the key exists - Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` // The integer value (if exists is true) -} - -func (x *GetIntResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetIntResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -func (x *GetIntResponse) GetValue() int64 { - if x != nil { - return x.Value - } - return 0 -} - -// Response containing a float value -type GetFloatResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Whether the key exists - Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // The float value (if exists is true) -} - -func (x *GetFloatResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetFloatResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -func (x *GetFloatResponse) GetValue() float64 { - if x != nil { - return x.Value - } - return 0 -} - -// Response containing a byte slice value -type GetBytesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Whether the key exists - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // The byte slice value (if exists is true) -} - -func (x *GetBytesResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetBytesResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -func (x *GetBytesResponse) GetValue() []byte { - if x != nil { - return x.Value - } - return nil -} - -// Request to remove a value -type RemoveRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key -} - -func (x *RemoveRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *RemoveRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -// Response after removing a value -type RemoveResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` // Whether the operation was successful -} - -func (x *RemoveResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *RemoveResponse) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -// Request to check if a key exists -type HasRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Cache key -} - -func (x *HasRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *HasRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -// Response indicating if a key exists -type HasResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Whether the key exists -} - -func (x *HasResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *HasResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -// go:plugin type=host version=1 -type CacheService interface { - // Set a string value in the cache - SetString(context.Context, *SetStringRequest) (*SetResponse, error) - // Get a string value from the cache - GetString(context.Context, *GetRequest) (*GetStringResponse, error) - // Set an integer value in the cache - SetInt(context.Context, *SetIntRequest) (*SetResponse, error) - // Get an integer value from the cache - GetInt(context.Context, *GetRequest) (*GetIntResponse, error) - // Set a float value in the cache - SetFloat(context.Context, *SetFloatRequest) (*SetResponse, error) - // Get a float value from the cache - GetFloat(context.Context, *GetRequest) (*GetFloatResponse, error) - // Set a byte slice value in the cache - SetBytes(context.Context, *SetBytesRequest) (*SetResponse, error) - // Get a byte slice value from the cache - GetBytes(context.Context, *GetRequest) (*GetBytesResponse, error) - // Remove a value from the cache - Remove(context.Context, *RemoveRequest) (*RemoveResponse, error) - // Check if a key exists in the cache - Has(context.Context, *HasRequest) (*HasResponse, error) -} diff --git a/plugins/host/cache/cache.proto b/plugins/host/cache/cache.proto deleted file mode 100644 index 8081eca3d..000000000 --- a/plugins/host/cache/cache.proto +++ /dev/null @@ -1,120 +0,0 @@ -syntax = "proto3"; - -package cache; - -option go_package = "github.com/navidrome/navidrome/plugins/host/cache;cache"; - -// go:plugin type=host version=1 -service CacheService { - // Set a string value in the cache - rpc SetString(SetStringRequest) returns (SetResponse); - - // Get a string value from the cache - rpc GetString(GetRequest) returns (GetStringResponse); - - // Set an integer value in the cache - rpc SetInt(SetIntRequest) returns (SetResponse); - - // Get an integer value from the cache - rpc GetInt(GetRequest) returns (GetIntResponse); - - // Set a float value in the cache - rpc SetFloat(SetFloatRequest) returns (SetResponse); - - // Get a float value from the cache - rpc GetFloat(GetRequest) returns (GetFloatResponse); - - // Set a byte slice value in the cache - rpc SetBytes(SetBytesRequest) returns (SetResponse); - - // Get a byte slice value from the cache - rpc GetBytes(GetRequest) returns (GetBytesResponse); - - // Remove a value from the cache - rpc Remove(RemoveRequest) returns (RemoveResponse); - - // Check if a key exists in the cache - rpc Has(HasRequest) returns (HasResponse); -} - -// Request to store a string value -message SetStringRequest { - string key = 1; // Cache key - string value = 2; // String value to store - int64 ttl_seconds = 3; // TTL in seconds, 0 means use default -} - -// Request to store an integer value -message SetIntRequest { - string key = 1; // Cache key - int64 value = 2; // Integer value to store - int64 ttl_seconds = 3; // TTL in seconds, 0 means use default -} - -// Request to store a float value -message SetFloatRequest { - string key = 1; // Cache key - double value = 2; // Float value to store - int64 ttl_seconds = 3; // TTL in seconds, 0 means use default -} - -// Request to store a byte slice value -message SetBytesRequest { - string key = 1; // Cache key - bytes value = 2; // Byte slice value to store - int64 ttl_seconds = 3; // TTL in seconds, 0 means use default -} - -// Response after setting a value -message SetResponse { - bool success = 1; // Whether the operation was successful -} - -// Request to get a value -message GetRequest { - string key = 1; // Cache key -} - -// Response containing a string value -message GetStringResponse { - bool exists = 1; // Whether the key exists - string value = 2; // The string value (if exists is true) -} - -// Response containing an integer value -message GetIntResponse { - bool exists = 1; // Whether the key exists - int64 value = 2; // The integer value (if exists is true) -} - -// Response containing a float value -message GetFloatResponse { - bool exists = 1; // Whether the key exists - double value = 2; // The float value (if exists is true) -} - -// Response containing a byte slice value -message GetBytesResponse { - bool exists = 1; // Whether the key exists - bytes value = 2; // The byte slice value (if exists is true) -} - -// Request to remove a value -message RemoveRequest { - string key = 1; // Cache key -} - -// Response after removing a value -message RemoveResponse { - bool success = 1; // Whether the operation was successful -} - -// Request to check if a key exists -message HasRequest { - string key = 1; // Cache key -} - -// Response indicating if a key exists -message HasResponse { - bool exists = 1; // Whether the key exists -} \ No newline at end of file diff --git a/plugins/host/cache/cache_host.pb.go b/plugins/host/cache/cache_host.pb.go deleted file mode 100644 index 479473fa8..000000000 --- a/plugins/host/cache/cache_host.pb.go +++ /dev/null @@ -1,374 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/cache/cache.proto - -package cache - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _cacheService struct { - CacheService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions CacheService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _cacheService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._SetString), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("set_string") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetString), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_string") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._SetInt), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("set_int") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetInt), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_int") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._SetFloat), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("set_float") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetFloat), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_float") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._SetBytes), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("set_bytes") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetBytes), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_bytes") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Remove), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("remove") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Has), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("has") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -// Set a string value in the cache - -func (h _cacheService) _SetString(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(SetStringRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.SetString(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Get a string value from the cache - -func (h _cacheService) _GetString(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetString(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Set an integer value in the cache - -func (h _cacheService) _SetInt(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(SetIntRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.SetInt(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Get an integer value from the cache - -func (h _cacheService) _GetInt(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetInt(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Set a float value in the cache - -func (h _cacheService) _SetFloat(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(SetFloatRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.SetFloat(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Get a float value from the cache - -func (h _cacheService) _GetFloat(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetFloat(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Set a byte slice value in the cache - -func (h _cacheService) _SetBytes(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(SetBytesRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.SetBytes(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Get a byte slice value from the cache - -func (h _cacheService) _GetBytes(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetBytes(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Remove a value from the cache - -func (h _cacheService) _Remove(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(RemoveRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Remove(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Check if a key exists in the cache - -func (h _cacheService) _Has(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HasRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Has(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/cache/cache_plugin.pb.go b/plugins/host/cache/cache_plugin.pb.go deleted file mode 100644 index 6e3bdcd44..000000000 --- a/plugins/host/cache/cache_plugin.pb.go +++ /dev/null @@ -1,251 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/cache/cache.proto - -package cache - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type cacheService struct{} - -func NewCacheService() CacheService { - return cacheService{} -} - -//go:wasmimport env set_string -func _set_string(ptr uint32, size uint32) uint64 - -func (h cacheService) SetString(ctx context.Context, request *SetStringRequest) (*SetResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _set_string(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(SetResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env get_string -func _get_string(ptr uint32, size uint32) uint64 - -func (h cacheService) GetString(ctx context.Context, request *GetRequest) (*GetStringResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_string(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetStringResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env set_int -func _set_int(ptr uint32, size uint32) uint64 - -func (h cacheService) SetInt(ctx context.Context, request *SetIntRequest) (*SetResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _set_int(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(SetResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env get_int -func _get_int(ptr uint32, size uint32) uint64 - -func (h cacheService) GetInt(ctx context.Context, request *GetRequest) (*GetIntResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_int(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetIntResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env set_float -func _set_float(ptr uint32, size uint32) uint64 - -func (h cacheService) SetFloat(ctx context.Context, request *SetFloatRequest) (*SetResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _set_float(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(SetResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env get_float -func _get_float(ptr uint32, size uint32) uint64 - -func (h cacheService) GetFloat(ctx context.Context, request *GetRequest) (*GetFloatResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_float(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetFloatResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env set_bytes -func _set_bytes(ptr uint32, size uint32) uint64 - -func (h cacheService) SetBytes(ctx context.Context, request *SetBytesRequest) (*SetResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _set_bytes(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(SetResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env get_bytes -func _get_bytes(ptr uint32, size uint32) uint64 - -func (h cacheService) GetBytes(ctx context.Context, request *GetRequest) (*GetBytesResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_bytes(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetBytesResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env remove -func _remove(ptr uint32, size uint32) uint64 - -func (h cacheService) Remove(ctx context.Context, request *RemoveRequest) (*RemoveResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _remove(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(RemoveResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env has -func _has(ptr uint32, size uint32) uint64 - -func (h cacheService) Has(ctx context.Context, request *HasRequest) (*HasResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _has(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HasResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/cache/cache_plugin_dev.go b/plugins/host/cache/cache_plugin_dev.go deleted file mode 100644 index 824dcc71d..000000000 --- a/plugins/host/cache/cache_plugin_dev.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !wasip1 - -package cache - -func NewCacheService() CacheService { - panic("not implemented") -} diff --git a/plugins/host/cache/cache_vtproto.pb.go b/plugins/host/cache/cache_vtproto.pb.go deleted file mode 100644 index 0ee3d9f22..000000000 --- a/plugins/host/cache/cache_vtproto.pb.go +++ /dev/null @@ -1,2352 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/cache/cache.proto - -package cache - -import ( - binary "encoding/binary" - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - math "math" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *SetStringRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SetStringRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SetStringRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.TtlSeconds != 0 { - i = encodeVarint(dAtA, i, uint64(m.TtlSeconds)) - i-- - dAtA[i] = 0x18 - } - if len(m.Value) > 0 { - i -= len(m.Value) - copy(dAtA[i:], m.Value) - i = encodeVarint(dAtA, i, uint64(len(m.Value))) - i-- - dAtA[i] = 0x12 - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SetIntRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SetIntRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SetIntRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.TtlSeconds != 0 { - i = encodeVarint(dAtA, i, uint64(m.TtlSeconds)) - i-- - dAtA[i] = 0x18 - } - if m.Value != 0 { - i = encodeVarint(dAtA, i, uint64(m.Value)) - i-- - dAtA[i] = 0x10 - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SetFloatRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SetFloatRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SetFloatRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.TtlSeconds != 0 { - i = encodeVarint(dAtA, i, uint64(m.TtlSeconds)) - i-- - dAtA[i] = 0x18 - } - if m.Value != 0 { - i -= 8 - binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Value)))) - i-- - dAtA[i] = 0x11 - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SetBytesRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SetBytesRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SetBytesRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.TtlSeconds != 0 { - i = encodeVarint(dAtA, i, uint64(m.TtlSeconds)) - i-- - dAtA[i] = 0x18 - } - if len(m.Value) > 0 { - i -= len(m.Value) - copy(dAtA[i:], m.Value) - i = encodeVarint(dAtA, i, uint64(len(m.Value))) - i-- - dAtA[i] = 0x12 - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SetResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SetResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SetResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Success { - i-- - if m.Success { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *GetRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *GetStringResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetStringResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetStringResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Value) > 0 { - i -= len(m.Value) - copy(dAtA[i:], m.Value) - i = encodeVarint(dAtA, i, uint64(len(m.Value))) - i-- - dAtA[i] = 0x12 - } - if m.Exists { - i-- - if m.Exists { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *GetIntResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetIntResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetIntResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Value != 0 { - i = encodeVarint(dAtA, i, uint64(m.Value)) - i-- - dAtA[i] = 0x10 - } - if m.Exists { - i-- - if m.Exists { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *GetFloatResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetFloatResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetFloatResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Value != 0 { - i -= 8 - binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.Value)))) - i-- - dAtA[i] = 0x11 - } - if m.Exists { - i-- - if m.Exists { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *GetBytesResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetBytesResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetBytesResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Value) > 0 { - i -= len(m.Value) - copy(dAtA[i:], m.Value) - i = encodeVarint(dAtA, i, uint64(len(m.Value))) - i-- - dAtA[i] = 0x12 - } - if m.Exists { - i-- - if m.Exists { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *RemoveRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *RemoveRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *RemoveRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *RemoveResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *RemoveResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *RemoveResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Success { - i-- - if m.Success { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *HasRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *HasRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *HasRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarint(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *HasResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *HasResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *HasResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if m.Exists { - i-- - if m.Exists { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *SetStringRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Value) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.TtlSeconds != 0 { - n += 1 + sov(uint64(m.TtlSeconds)) - } - n += len(m.unknownFields) - return n -} - -func (m *SetIntRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Value != 0 { - n += 1 + sov(uint64(m.Value)) - } - if m.TtlSeconds != 0 { - n += 1 + sov(uint64(m.TtlSeconds)) - } - n += len(m.unknownFields) - return n -} - -func (m *SetFloatRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Value != 0 { - n += 9 - } - if m.TtlSeconds != 0 { - n += 1 + sov(uint64(m.TtlSeconds)) - } - n += len(m.unknownFields) - return n -} - -func (m *SetBytesRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Value) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.TtlSeconds != 0 { - n += 1 + sov(uint64(m.TtlSeconds)) - } - n += len(m.unknownFields) - return n -} - -func (m *SetResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Success { - n += 2 - } - n += len(m.unknownFields) - return n -} - -func (m *GetRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *GetStringResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Exists { - n += 2 - } - l = len(m.Value) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *GetIntResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Exists { - n += 2 - } - if m.Value != 0 { - n += 1 + sov(uint64(m.Value)) - } - n += len(m.unknownFields) - return n -} - -func (m *GetFloatResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Exists { - n += 2 - } - if m.Value != 0 { - n += 9 - } - n += len(m.unknownFields) - return n -} - -func (m *GetBytesResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Exists { - n += 2 - } - l = len(m.Value) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *RemoveRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *RemoveResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Success { - n += 2 - } - n += len(m.unknownFields) - return n -} - -func (m *HasRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *HasResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Exists { - n += 2 - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *SetStringRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SetStringRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SetStringRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field TtlSeconds", wireType) - } - m.TtlSeconds = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.TtlSeconds |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SetIntRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SetIntRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SetIntRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - m.Value = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Value |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field TtlSeconds", wireType) - } - m.TtlSeconds = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.TtlSeconds |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SetFloatRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SetFloatRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SetFloatRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var v uint64 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - v = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 - m.Value = float64(math.Float64frombits(v)) - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field TtlSeconds", wireType) - } - m.TtlSeconds = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.TtlSeconds |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SetBytesRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SetBytesRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SetBytesRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = append(m.Value[:0], dAtA[iNdEx:postIndex]...) - if m.Value == nil { - m.Value = []byte{} - } - iNdEx = postIndex - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field TtlSeconds", wireType) - } - m.TtlSeconds = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.TtlSeconds |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SetResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SetResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SetResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Success", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Success = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetStringResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetStringResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetStringResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Exists", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Exists = bool(v != 0) - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetIntResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetIntResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetIntResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Exists", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Exists = bool(v != 0) - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - m.Value = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Value |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetFloatResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetFloatResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetFloatResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Exists", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Exists = bool(v != 0) - case 2: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var v uint64 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - v = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 - m.Value = float64(math.Float64frombits(v)) - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetBytesResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetBytesResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetBytesResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Exists", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Exists = bool(v != 0) - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = append(m.Value[:0], dAtA[iNdEx:postIndex]...) - if m.Value == nil { - m.Value = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *RemoveRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: RemoveRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: RemoveRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *RemoveResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: RemoveResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: RemoveResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Success", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Success = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *HasRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: HasRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: HasRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *HasResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: HasResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: HasResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Exists", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Exists = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host/config/config.pb.go b/plugins/host/config/config.pb.go deleted file mode 100644 index dfc70af19..000000000 --- a/plugins/host/config/config.pb.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/config/config.proto - -package config - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetPluginConfigRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *GetPluginConfigRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -type GetPluginConfigResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Config map[string]string `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` -} - -func (x *GetPluginConfigResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *GetPluginConfigResponse) GetConfig() map[string]string { - if x != nil { - return x.Config - } - return nil -} - -// go:plugin type=host version=1 -type ConfigService interface { - GetPluginConfig(context.Context, *GetPluginConfigRequest) (*GetPluginConfigResponse, error) -} diff --git a/plugins/host/config/config.proto b/plugins/host/config/config.proto deleted file mode 100644 index 76076b47b..000000000 --- a/plugins/host/config/config.proto +++ /dev/null @@ -1,18 +0,0 @@ -syntax = "proto3"; - -package config; - -option go_package = "github.com/navidrome/navidrome/plugins/host/config;config"; - -// go:plugin type=host version=1 -service ConfigService { - rpc GetPluginConfig(GetPluginConfigRequest) returns (GetPluginConfigResponse); -} - -message GetPluginConfigRequest { - // No fields needed; plugin name is inferred from context -} - -message GetPluginConfigResponse { - map<string, string> config = 1; -} \ No newline at end of file diff --git a/plugins/host/config/config_host.pb.go b/plugins/host/config/config_host.pb.go deleted file mode 100644 index 87894f1a2..000000000 --- a/plugins/host/config/config_host.pb.go +++ /dev/null @@ -1,66 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/config/config.proto - -package config - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _configService struct { - ConfigService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions ConfigService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _configService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._GetPluginConfig), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get_plugin_config") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -func (h _configService) _GetPluginConfig(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(GetPluginConfigRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.GetPluginConfig(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/config/config_plugin.pb.go b/plugins/host/config/config_plugin.pb.go deleted file mode 100644 index 45c60d13a..000000000 --- a/plugins/host/config/config_plugin.pb.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/config/config.proto - -package config - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type configService struct{} - -func NewConfigService() ConfigService { - return configService{} -} - -//go:wasmimport env get_plugin_config -func _get_plugin_config(ptr uint32, size uint32) uint64 - -func (h configService) GetPluginConfig(ctx context.Context, request *GetPluginConfigRequest) (*GetPluginConfigResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get_plugin_config(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(GetPluginConfigResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/config/config_plugin_dev.go b/plugins/host/config/config_plugin_dev.go deleted file mode 100644 index dddbc9ceb..000000000 --- a/plugins/host/config/config_plugin_dev.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !wasip1 - -package config - -func NewConfigService() ConfigService { - panic("not implemented") -} diff --git a/plugins/host/config/config_vtproto.pb.go b/plugins/host/config/config_vtproto.pb.go deleted file mode 100644 index 295da164d..000000000 --- a/plugins/host/config/config_vtproto.pb.go +++ /dev/null @@ -1,466 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/config/config.proto - -package config - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *GetPluginConfigRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetPluginConfigRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetPluginConfigRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - return len(dAtA) - i, nil -} - -func (m *GetPluginConfigResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *GetPluginConfigResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *GetPluginConfigResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Config) > 0 { - for k := range m.Config { - v := m.Config[k] - baseI := i - i -= len(v) - copy(dAtA[i:], v) - i = encodeVarint(dAtA, i, uint64(len(v))) - i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = encodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = encodeVarint(dAtA, i, uint64(baseI-i)) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *GetPluginConfigRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - n += len(m.unknownFields) - return n -} - -func (m *GetPluginConfigResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Config) > 0 { - for k, v := range m.Config { - _ = k - _ = v - mapEntrySize := 1 + len(k) + sov(uint64(len(k))) + 1 + len(v) + sov(uint64(len(v))) - n += mapEntrySize + 1 + sov(uint64(mapEntrySize)) - } - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *GetPluginConfigRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetPluginConfigRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetPluginConfigRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *GetPluginConfigResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: GetPluginConfigResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: GetPluginConfigResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Config", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Config == nil { - m.Config = make(map[string]string) - } - var mapkey string - var mapvalue string - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var stringLenmapvalue uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapvalue |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapvalue := int(stringLenmapvalue) - if intStringLenmapvalue < 0 { - return ErrInvalidLength - } - postStringIndexmapvalue := iNdEx + intStringLenmapvalue - if postStringIndexmapvalue < 0 { - return ErrInvalidLength - } - if postStringIndexmapvalue > l { - return io.ErrUnexpectedEOF - } - mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) - iNdEx = postStringIndexmapvalue - } else { - iNdEx = entryPreIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Config[mapkey] = mapvalue - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host/http/http.pb.go b/plugins/host/http/http.pb.go deleted file mode 100644 index 0bc2c5040..000000000 --- a/plugins/host/http/http.pb.go +++ /dev/null @@ -1,117 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/http/http.proto - -package http - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type HttpRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - Headers map[string]string `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - TimeoutMs int32 `protobuf:"varint,3,opt,name=timeout_ms,json=timeoutMs,proto3" json:"timeout_ms,omitempty"` - Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` // Ignored for GET/DELETE/HEAD/OPTIONS -} - -func (x *HttpRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *HttpRequest) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -func (x *HttpRequest) GetHeaders() map[string]string { - if x != nil { - return x.Headers - } - return nil -} - -func (x *HttpRequest) GetTimeoutMs() int32 { - if x != nil { - return x.TimeoutMs - } - return 0 -} - -func (x *HttpRequest) GetBody() []byte { - if x != nil { - return x.Body - } - return nil -} - -type HttpResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Status int32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` - Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` - Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` // Non-empty if network/protocol error -} - -func (x *HttpResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *HttpResponse) GetStatus() int32 { - if x != nil { - return x.Status - } - return 0 -} - -func (x *HttpResponse) GetBody() []byte { - if x != nil { - return x.Body - } - return nil -} - -func (x *HttpResponse) GetHeaders() map[string]string { - if x != nil { - return x.Headers - } - return nil -} - -func (x *HttpResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -// go:plugin type=host version=1 -type HttpService interface { - Get(context.Context, *HttpRequest) (*HttpResponse, error) - Post(context.Context, *HttpRequest) (*HttpResponse, error) - Put(context.Context, *HttpRequest) (*HttpResponse, error) - Delete(context.Context, *HttpRequest) (*HttpResponse, error) - Patch(context.Context, *HttpRequest) (*HttpResponse, error) - Head(context.Context, *HttpRequest) (*HttpResponse, error) - Options(context.Context, *HttpRequest) (*HttpResponse, error) -} diff --git a/plugins/host/http/http.proto b/plugins/host/http/http.proto deleted file mode 100644 index 2ed7a4262..000000000 --- a/plugins/host/http/http.proto +++ /dev/null @@ -1,30 +0,0 @@ -syntax = "proto3"; - -package http; - -option go_package = "github.com/navidrome/navidrome/plugins/host/http;http"; - -// go:plugin type=host version=1 -service HttpService { - rpc Get(HttpRequest) returns (HttpResponse); - rpc Post(HttpRequest) returns (HttpResponse); - rpc Put(HttpRequest) returns (HttpResponse); - rpc Delete(HttpRequest) returns (HttpResponse); - rpc Patch(HttpRequest) returns (HttpResponse); - rpc Head(HttpRequest) returns (HttpResponse); - rpc Options(HttpRequest) returns (HttpResponse); -} - -message HttpRequest { - string url = 1; - map<string, string> headers = 2; - int32 timeout_ms = 3; - bytes body = 4; // Ignored for GET/DELETE/HEAD/OPTIONS -} - -message HttpResponse { - int32 status = 1; - bytes body = 2; - map<string, string> headers = 3; - string error = 4; // Non-empty if network/protocol error -} \ No newline at end of file diff --git a/plugins/host/http/http_host.pb.go b/plugins/host/http/http_host.pb.go deleted file mode 100644 index 326aba508..000000000 --- a/plugins/host/http/http_host.pb.go +++ /dev/null @@ -1,258 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/http/http.proto - -package http - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _httpService struct { - HttpService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions HttpService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _httpService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Get), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("get") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Post), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("post") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Put), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("put") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Delete), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("delete") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Patch), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("patch") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Head), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("head") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Options), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("options") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -func (h _httpService) _Get(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Get(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _httpService) _Post(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Post(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _httpService) _Put(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Put(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _httpService) _Delete(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Delete(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _httpService) _Patch(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Patch(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _httpService) _Head(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Head(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -func (h _httpService) _Options(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(HttpRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Options(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/http/http_plugin.pb.go b/plugins/host/http/http_plugin.pb.go deleted file mode 100644 index 2e8c21891..000000000 --- a/plugins/host/http/http_plugin.pb.go +++ /dev/null @@ -1,182 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/http/http.proto - -package http - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type httpService struct{} - -func NewHttpService() HttpService { - return httpService{} -} - -//go:wasmimport env get -func _get(ptr uint32, size uint32) uint64 - -func (h httpService) Get(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _get(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env post -func _post(ptr uint32, size uint32) uint64 - -func (h httpService) Post(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _post(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env put -func _put(ptr uint32, size uint32) uint64 - -func (h httpService) Put(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _put(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env delete -func _delete(ptr uint32, size uint32) uint64 - -func (h httpService) Delete(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _delete(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env patch -func _patch(ptr uint32, size uint32) uint64 - -func (h httpService) Patch(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _patch(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env head -func _head(ptr uint32, size uint32) uint64 - -func (h httpService) Head(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _head(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env options -func _options(ptr uint32, size uint32) uint64 - -func (h httpService) Options(ctx context.Context, request *HttpRequest) (*HttpResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _options(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(HttpResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/http/http_plugin_dev.go b/plugins/host/http/http_plugin_dev.go deleted file mode 100644 index 04e3c2508..000000000 --- a/plugins/host/http/http_plugin_dev.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !wasip1 - -package http - -func NewHttpService() HttpService { - panic("not implemented") -} diff --git a/plugins/host/http/http_vtproto.pb.go b/plugins/host/http/http_vtproto.pb.go deleted file mode 100644 index 064fdb08a..000000000 --- a/plugins/host/http/http_vtproto.pb.go +++ /dev/null @@ -1,850 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/http/http.proto - -package http - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *HttpRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *HttpRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *HttpRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Body) > 0 { - i -= len(m.Body) - copy(dAtA[i:], m.Body) - i = encodeVarint(dAtA, i, uint64(len(m.Body))) - i-- - dAtA[i] = 0x22 - } - if m.TimeoutMs != 0 { - i = encodeVarint(dAtA, i, uint64(m.TimeoutMs)) - i-- - dAtA[i] = 0x18 - } - if len(m.Headers) > 0 { - for k := range m.Headers { - v := m.Headers[k] - baseI := i - i -= len(v) - copy(dAtA[i:], v) - i = encodeVarint(dAtA, i, uint64(len(v))) - i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = encodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = encodeVarint(dAtA, i, uint64(baseI-i)) - i-- - dAtA[i] = 0x12 - } - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *HttpResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *HttpResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *HttpResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0x22 - } - if len(m.Headers) > 0 { - for k := range m.Headers { - v := m.Headers[k] - baseI := i - i -= len(v) - copy(dAtA[i:], v) - i = encodeVarint(dAtA, i, uint64(len(v))) - i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = encodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = encodeVarint(dAtA, i, uint64(baseI-i)) - i-- - dAtA[i] = 0x1a - } - } - if len(m.Body) > 0 { - i -= len(m.Body) - copy(dAtA[i:], m.Body) - i = encodeVarint(dAtA, i, uint64(len(m.Body))) - i-- - dAtA[i] = 0x12 - } - if m.Status != 0 { - i = encodeVarint(dAtA, i, uint64(m.Status)) - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *HttpRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if len(m.Headers) > 0 { - for k, v := range m.Headers { - _ = k - _ = v - mapEntrySize := 1 + len(k) + sov(uint64(len(k))) + 1 + len(v) + sov(uint64(len(v))) - n += mapEntrySize + 1 + sov(uint64(mapEntrySize)) - } - } - if m.TimeoutMs != 0 { - n += 1 + sov(uint64(m.TimeoutMs)) - } - l = len(m.Body) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *HttpResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Status != 0 { - n += 1 + sov(uint64(m.Status)) - } - l = len(m.Body) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if len(m.Headers) > 0 { - for k, v := range m.Headers { - _ = k - _ = v - mapEntrySize := 1 + len(k) + sov(uint64(len(k))) + 1 + len(v) + sov(uint64(len(v))) - n += mapEntrySize + 1 + sov(uint64(mapEntrySize)) - } - } - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *HttpRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: HttpRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: HttpRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Headers", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Headers == nil { - m.Headers = make(map[string]string) - } - var mapkey string - var mapvalue string - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var stringLenmapvalue uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapvalue |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapvalue := int(stringLenmapvalue) - if intStringLenmapvalue < 0 { - return ErrInvalidLength - } - postStringIndexmapvalue := iNdEx + intStringLenmapvalue - if postStringIndexmapvalue < 0 { - return ErrInvalidLength - } - if postStringIndexmapvalue > l { - return io.ErrUnexpectedEOF - } - mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) - iNdEx = postStringIndexmapvalue - } else { - iNdEx = entryPreIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Headers[mapkey] = mapvalue - iNdEx = postIndex - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field TimeoutMs", wireType) - } - m.TimeoutMs = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.TimeoutMs |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Body", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Body = append(m.Body[:0], dAtA[iNdEx:postIndex]...) - if m.Body == nil { - m.Body = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *HttpResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: HttpResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: HttpResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType) - } - m.Status = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Status |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Body", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Body = append(m.Body[:0], dAtA[iNdEx:postIndex]...) - if m.Body == nil { - m.Body = []byte{} - } - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Headers", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Headers == nil { - m.Headers = make(map[string]string) - } - var mapkey string - var mapvalue string - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var stringLenmapvalue uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapvalue |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapvalue := int(stringLenmapvalue) - if intStringLenmapvalue < 0 { - return ErrInvalidLength - } - postStringIndexmapvalue := iNdEx + intStringLenmapvalue - if postStringIndexmapvalue < 0 { - return ErrInvalidLength - } - if postStringIndexmapvalue > l { - return io.ErrUnexpectedEOF - } - mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) - iNdEx = postStringIndexmapvalue - } else { - iNdEx = entryPreIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Headers[mapkey] = mapvalue - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host/scheduler/scheduler.pb.go b/plugins/host/scheduler/scheduler.pb.go deleted file mode 100644 index 07d250cc5..000000000 --- a/plugins/host/scheduler/scheduler.pb.go +++ /dev/null @@ -1,212 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/scheduler/scheduler.proto - -package scheduler - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ScheduleOneTimeRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - DelaySeconds int32 `protobuf:"varint,1,opt,name=delay_seconds,json=delaySeconds,proto3" json:"delay_seconds,omitempty"` // Delay in seconds - Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // Serialized data to pass to the callback - ScheduleId string `protobuf:"bytes,3,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` // Optional custom ID (if not provided, one will be generated) -} - -func (x *ScheduleOneTimeRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScheduleOneTimeRequest) GetDelaySeconds() int32 { - if x != nil { - return x.DelaySeconds - } - return 0 -} - -func (x *ScheduleOneTimeRequest) GetPayload() []byte { - if x != nil { - return x.Payload - } - return nil -} - -func (x *ScheduleOneTimeRequest) GetScheduleId() string { - if x != nil { - return x.ScheduleId - } - return "" -} - -type ScheduleRecurringRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - CronExpression string `protobuf:"bytes,1,opt,name=cron_expression,json=cronExpression,proto3" json:"cron_expression,omitempty"` // Cron expression (e.g. "0 0 * * *" for daily at midnight) - Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // Serialized data to pass to the callback - ScheduleId string `protobuf:"bytes,3,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` // Optional custom ID (if not provided, one will be generated) -} - -func (x *ScheduleRecurringRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScheduleRecurringRequest) GetCronExpression() string { - if x != nil { - return x.CronExpression - } - return "" -} - -func (x *ScheduleRecurringRequest) GetPayload() []byte { - if x != nil { - return x.Payload - } - return nil -} - -func (x *ScheduleRecurringRequest) GetScheduleId() string { - if x != nil { - return x.ScheduleId - } - return "" -} - -type ScheduleResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ScheduleId string `protobuf:"bytes,1,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` // ID to reference this scheduled job -} - -func (x *ScheduleResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ScheduleResponse) GetScheduleId() string { - if x != nil { - return x.ScheduleId - } - return "" -} - -type CancelRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ScheduleId string `protobuf:"bytes,1,opt,name=schedule_id,json=scheduleId,proto3" json:"schedule_id,omitempty"` // ID of the schedule to cancel -} - -func (x *CancelRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *CancelRequest) GetScheduleId() string { - if x != nil { - return x.ScheduleId - } - return "" -} - -type CancelResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` // Whether cancellation was successful - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Error message if cancellation failed -} - -func (x *CancelResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *CancelResponse) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -func (x *CancelResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type TimeNowRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *TimeNowRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -type TimeNowResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Rfc3339Nano string `protobuf:"bytes,1,opt,name=rfc3339_nano,json=rfc3339Nano,proto3" json:"rfc3339_nano,omitempty"` // Current time in RFC3339Nano format - UnixMilli int64 `protobuf:"varint,2,opt,name=unix_milli,json=unixMilli,proto3" json:"unix_milli,omitempty"` // Current time as Unix milliseconds timestamp - LocalTimeZone string `protobuf:"bytes,3,opt,name=local_time_zone,json=localTimeZone,proto3" json:"local_time_zone,omitempty"` // Local timezone name (e.g., "America/New_York", "UTC") -} - -func (x *TimeNowResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *TimeNowResponse) GetRfc3339Nano() string { - if x != nil { - return x.Rfc3339Nano - } - return "" -} - -func (x *TimeNowResponse) GetUnixMilli() int64 { - if x != nil { - return x.UnixMilli - } - return 0 -} - -func (x *TimeNowResponse) GetLocalTimeZone() string { - if x != nil { - return x.LocalTimeZone - } - return "" -} - -// go:plugin type=host version=1 -type SchedulerService interface { - // One-time event scheduling - ScheduleOneTime(context.Context, *ScheduleOneTimeRequest) (*ScheduleResponse, error) - // Recurring event scheduling - ScheduleRecurring(context.Context, *ScheduleRecurringRequest) (*ScheduleResponse, error) - // Cancel any scheduled job - CancelSchedule(context.Context, *CancelRequest) (*CancelResponse, error) - // Get current time in multiple formats - TimeNow(context.Context, *TimeNowRequest) (*TimeNowResponse, error) -} diff --git a/plugins/host/scheduler/scheduler.proto b/plugins/host/scheduler/scheduler.proto deleted file mode 100644 index d164b4f90..000000000 --- a/plugins/host/scheduler/scheduler.proto +++ /dev/null @@ -1,55 +0,0 @@ -syntax = "proto3"; - -package scheduler; - -option go_package = "github.com/navidrome/navidrome/plugins/host/scheduler;scheduler"; - -// go:plugin type=host version=1 -service SchedulerService { - // One-time event scheduling - rpc ScheduleOneTime(ScheduleOneTimeRequest) returns (ScheduleResponse); - - // Recurring event scheduling - rpc ScheduleRecurring(ScheduleRecurringRequest) returns (ScheduleResponse); - - // Cancel any scheduled job - rpc CancelSchedule(CancelRequest) returns (CancelResponse); - - // Get current time in multiple formats - rpc TimeNow(TimeNowRequest) returns (TimeNowResponse); -} - -message ScheduleOneTimeRequest { - int32 delay_seconds = 1; // Delay in seconds - bytes payload = 2; // Serialized data to pass to the callback - string schedule_id = 3; // Optional custom ID (if not provided, one will be generated) -} - -message ScheduleRecurringRequest { - string cron_expression = 1; // Cron expression (e.g. "0 0 * * *" for daily at midnight) - bytes payload = 2; // Serialized data to pass to the callback - string schedule_id = 3; // Optional custom ID (if not provided, one will be generated) -} - -message ScheduleResponse { - string schedule_id = 1; // ID to reference this scheduled job -} - -message CancelRequest { - string schedule_id = 1; // ID of the schedule to cancel -} - -message CancelResponse { - bool success = 1; // Whether cancellation was successful - string error = 2; // Error message if cancellation failed -} - -message TimeNowRequest { - // Empty request - no parameters needed -} - -message TimeNowResponse { - string rfc3339_nano = 1; // Current time in RFC3339Nano format - int64 unix_milli = 2; // Current time as Unix milliseconds timestamp - string local_time_zone = 3; // Local timezone name (e.g., "America/New_York", "UTC") -} \ No newline at end of file diff --git a/plugins/host/scheduler/scheduler_host.pb.go b/plugins/host/scheduler/scheduler_host.pb.go deleted file mode 100644 index 714603a3b..000000000 --- a/plugins/host/scheduler/scheduler_host.pb.go +++ /dev/null @@ -1,170 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/scheduler/scheduler.proto - -package scheduler - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _schedulerService struct { - SchedulerService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions SchedulerService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _schedulerService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._ScheduleOneTime), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("schedule_one_time") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._ScheduleRecurring), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("schedule_recurring") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._CancelSchedule), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("cancel_schedule") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._TimeNow), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("time_now") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -// One-time event scheduling - -func (h _schedulerService) _ScheduleOneTime(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(ScheduleOneTimeRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.ScheduleOneTime(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Recurring event scheduling - -func (h _schedulerService) _ScheduleRecurring(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(ScheduleRecurringRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.ScheduleRecurring(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Cancel any scheduled job - -func (h _schedulerService) _CancelSchedule(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(CancelRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.CancelSchedule(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Get current time in multiple formats - -func (h _schedulerService) _TimeNow(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(TimeNowRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.TimeNow(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/scheduler/scheduler_plugin.pb.go b/plugins/host/scheduler/scheduler_plugin.pb.go deleted file mode 100644 index ab7f8cd48..000000000 --- a/plugins/host/scheduler/scheduler_plugin.pb.go +++ /dev/null @@ -1,113 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/scheduler/scheduler.proto - -package scheduler - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type schedulerService struct{} - -func NewSchedulerService() SchedulerService { - return schedulerService{} -} - -//go:wasmimport env schedule_one_time -func _schedule_one_time(ptr uint32, size uint32) uint64 - -func (h schedulerService) ScheduleOneTime(ctx context.Context, request *ScheduleOneTimeRequest) (*ScheduleResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _schedule_one_time(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(ScheduleResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env schedule_recurring -func _schedule_recurring(ptr uint32, size uint32) uint64 - -func (h schedulerService) ScheduleRecurring(ctx context.Context, request *ScheduleRecurringRequest) (*ScheduleResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _schedule_recurring(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(ScheduleResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env cancel_schedule -func _cancel_schedule(ptr uint32, size uint32) uint64 - -func (h schedulerService) CancelSchedule(ctx context.Context, request *CancelRequest) (*CancelResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _cancel_schedule(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(CancelResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env time_now -func _time_now(ptr uint32, size uint32) uint64 - -func (h schedulerService) TimeNow(ctx context.Context, request *TimeNowRequest) (*TimeNowResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _time_now(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(TimeNowResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/scheduler/scheduler_plugin_dev.go b/plugins/host/scheduler/scheduler_plugin_dev.go deleted file mode 100644 index b6feaa8e4..000000000 --- a/plugins/host/scheduler/scheduler_plugin_dev.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !wasip1 - -package scheduler - -func NewSchedulerService() SchedulerService { - panic("not implemented") -} diff --git a/plugins/host/scheduler/scheduler_vtproto.pb.go b/plugins/host/scheduler/scheduler_vtproto.pb.go deleted file mode 100644 index ee6421783..000000000 --- a/plugins/host/scheduler/scheduler_vtproto.pb.go +++ /dev/null @@ -1,1303 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/scheduler/scheduler.proto - -package scheduler - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *ScheduleOneTimeRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScheduleOneTimeRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScheduleOneTimeRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.ScheduleId) > 0 { - i -= len(m.ScheduleId) - copy(dAtA[i:], m.ScheduleId) - i = encodeVarint(dAtA, i, uint64(len(m.ScheduleId))) - i-- - dAtA[i] = 0x1a - } - if len(m.Payload) > 0 { - i -= len(m.Payload) - copy(dAtA[i:], m.Payload) - i = encodeVarint(dAtA, i, uint64(len(m.Payload))) - i-- - dAtA[i] = 0x12 - } - if m.DelaySeconds != 0 { - i = encodeVarint(dAtA, i, uint64(m.DelaySeconds)) - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *ScheduleRecurringRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScheduleRecurringRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScheduleRecurringRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.ScheduleId) > 0 { - i -= len(m.ScheduleId) - copy(dAtA[i:], m.ScheduleId) - i = encodeVarint(dAtA, i, uint64(len(m.ScheduleId))) - i-- - dAtA[i] = 0x1a - } - if len(m.Payload) > 0 { - i -= len(m.Payload) - copy(dAtA[i:], m.Payload) - i = encodeVarint(dAtA, i, uint64(len(m.Payload))) - i-- - dAtA[i] = 0x12 - } - if len(m.CronExpression) > 0 { - i -= len(m.CronExpression) - copy(dAtA[i:], m.CronExpression) - i = encodeVarint(dAtA, i, uint64(len(m.CronExpression))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ScheduleResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ScheduleResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ScheduleResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.ScheduleId) > 0 { - i -= len(m.ScheduleId) - copy(dAtA[i:], m.ScheduleId) - i = encodeVarint(dAtA, i, uint64(len(m.ScheduleId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *CancelRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CancelRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *CancelRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.ScheduleId) > 0 { - i -= len(m.ScheduleId) - copy(dAtA[i:], m.ScheduleId) - i = encodeVarint(dAtA, i, uint64(len(m.ScheduleId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *CancelResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CancelResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *CancelResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0x12 - } - if m.Success { - i-- - if m.Success { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *TimeNowRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *TimeNowRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *TimeNowRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - return len(dAtA) - i, nil -} - -func (m *TimeNowResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *TimeNowResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *TimeNowResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.LocalTimeZone) > 0 { - i -= len(m.LocalTimeZone) - copy(dAtA[i:], m.LocalTimeZone) - i = encodeVarint(dAtA, i, uint64(len(m.LocalTimeZone))) - i-- - dAtA[i] = 0x1a - } - if m.UnixMilli != 0 { - i = encodeVarint(dAtA, i, uint64(m.UnixMilli)) - i-- - dAtA[i] = 0x10 - } - if len(m.Rfc3339Nano) > 0 { - i -= len(m.Rfc3339Nano) - copy(dAtA[i:], m.Rfc3339Nano) - i = encodeVarint(dAtA, i, uint64(len(m.Rfc3339Nano))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *ScheduleOneTimeRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.DelaySeconds != 0 { - n += 1 + sov(uint64(m.DelaySeconds)) - } - l = len(m.Payload) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.ScheduleId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScheduleRecurringRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.CronExpression) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Payload) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.ScheduleId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ScheduleResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ScheduleId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *CancelRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ScheduleId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *CancelResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Success { - n += 2 - } - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *TimeNowRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - n += len(m.unknownFields) - return n -} - -func (m *TimeNowResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Rfc3339Nano) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.UnixMilli != 0 { - n += 1 + sov(uint64(m.UnixMilli)) - } - l = len(m.LocalTimeZone) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *ScheduleOneTimeRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScheduleOneTimeRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScheduleOneTimeRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field DelaySeconds", wireType) - } - m.DelaySeconds = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.DelaySeconds |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Payload", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Payload = append(m.Payload[:0], dAtA[iNdEx:postIndex]...) - if m.Payload == nil { - m.Payload = []byte{} - } - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScheduleId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ScheduleId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScheduleRecurringRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScheduleRecurringRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScheduleRecurringRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field CronExpression", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.CronExpression = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Payload", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Payload = append(m.Payload[:0], dAtA[iNdEx:postIndex]...) - if m.Payload == nil { - m.Payload = []byte{} - } - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScheduleId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ScheduleId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ScheduleResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ScheduleResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ScheduleResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScheduleId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ScheduleId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *CancelRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CancelRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CancelRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScheduleId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ScheduleId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *CancelResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CancelResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CancelResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Success", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Success = bool(v != 0) - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *TimeNowRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: TimeNowRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: TimeNowRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *TimeNowResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: TimeNowResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: TimeNowResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Rfc3339Nano", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Rfc3339Nano = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field UnixMilli", wireType) - } - m.UnixMilli = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.UnixMilli |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field LocalTimeZone", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.LocalTimeZone = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host/subsonicapi/subsonicapi.pb.go b/plugins/host/subsonicapi/subsonicapi.pb.go deleted file mode 100644 index 0dbd9054f..000000000 --- a/plugins/host/subsonicapi/subsonicapi.pb.go +++ /dev/null @@ -1,71 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/subsonicapi/subsonicapi.proto - -package subsonicapi - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type CallRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` -} - -func (x *CallRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *CallRequest) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -type CallResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Json string `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Non-empty if operation failed -} - -func (x *CallResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *CallResponse) GetJson() string { - if x != nil { - return x.Json - } - return "" -} - -func (x *CallResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -// go:plugin type=host version=1 -type SubsonicAPIService interface { - Call(context.Context, *CallRequest) (*CallResponse, error) -} diff --git a/plugins/host/subsonicapi/subsonicapi.proto b/plugins/host/subsonicapi/subsonicapi.proto deleted file mode 100644 index 29dc365ca..000000000 --- a/plugins/host/subsonicapi/subsonicapi.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package subsonicapi; - -option go_package = "github.com/navidrome/navidrome/plugins/host/subsonicapi;subsonicapi"; - -// go:plugin type=host version=1 -service SubsonicAPIService { - rpc Call(CallRequest) returns (CallResponse); -} - -message CallRequest { - string url = 1; -} - -message CallResponse { - string json = 1; - string error = 2; // Non-empty if operation failed -} \ No newline at end of file diff --git a/plugins/host/subsonicapi/subsonicapi_host.pb.go b/plugins/host/subsonicapi/subsonicapi_host.pb.go deleted file mode 100644 index b7c0f042e..000000000 --- a/plugins/host/subsonicapi/subsonicapi_host.pb.go +++ /dev/null @@ -1,66 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/subsonicapi/subsonicapi.proto - -package subsonicapi - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _subsonicAPIService struct { - SubsonicAPIService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions SubsonicAPIService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _subsonicAPIService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Call), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("call") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -func (h _subsonicAPIService) _Call(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(CallRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Call(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/subsonicapi/subsonicapi_plugin.pb.go b/plugins/host/subsonicapi/subsonicapi_plugin.pb.go deleted file mode 100644 index 1ffdbf526..000000000 --- a/plugins/host/subsonicapi/subsonicapi_plugin.pb.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/subsonicapi/subsonicapi.proto - -package subsonicapi - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type subsonicAPIService struct{} - -func NewSubsonicAPIService() SubsonicAPIService { - return subsonicAPIService{} -} - -//go:wasmimport env call -func _call(ptr uint32, size uint32) uint64 - -func (h subsonicAPIService) Call(ctx context.Context, request *CallRequest) (*CallResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _call(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(CallResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/subsonicapi/subsonicapi_vtproto.pb.go b/plugins/host/subsonicapi/subsonicapi_vtproto.pb.go deleted file mode 100644 index 05403216b..000000000 --- a/plugins/host/subsonicapi/subsonicapi_vtproto.pb.go +++ /dev/null @@ -1,441 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/subsonicapi/subsonicapi.proto - -package subsonicapi - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *CallRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CallRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *CallRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *CallResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CallResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *CallResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0x12 - } - if len(m.Json) > 0 { - i -= len(m.Json) - copy(dAtA[i:], m.Json) - i = encodeVarint(dAtA, i, uint64(len(m.Json))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *CallRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *CallResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Json) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *CallRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CallRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CallRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *CallResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CallResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CallResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Json", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Json = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host/websocket/websocket.pb.go b/plugins/host/websocket/websocket.pb.go deleted file mode 100644 index f3ab68963..000000000 --- a/plugins/host/websocket/websocket.pb.go +++ /dev/null @@ -1,240 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/websocket/websocket.proto - -package websocket - -import ( - context "context" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ConnectRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - Headers map[string]string `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - ConnectionId string `protobuf:"bytes,3,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` -} - -func (x *ConnectRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ConnectRequest) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -func (x *ConnectRequest) GetHeaders() map[string]string { - if x != nil { - return x.Headers - } - return nil -} - -func (x *ConnectRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -type ConnectResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *ConnectResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *ConnectResponse) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *ConnectResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type SendTextRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *SendTextRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SendTextRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *SendTextRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type SendTextResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *SendTextResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SendTextResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type SendBinaryRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` -} - -func (x *SendBinaryRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SendBinaryRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *SendBinaryRequest) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -type SendBinaryResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *SendBinaryResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *SendBinaryResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type CloseRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ConnectionId string `protobuf:"bytes,1,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Code int32 `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"` - Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` -} - -func (x *CloseRequest) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *CloseRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *CloseRequest) GetCode() int32 { - if x != nil { - return x.Code - } - return 0 -} - -func (x *CloseRequest) GetReason() string { - if x != nil { - return x.Reason - } - return "" -} - -type CloseResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` -} - -func (x *CloseResponse) ProtoReflect() protoreflect.Message { - panic(`not implemented`) -} - -func (x *CloseResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -// go:plugin type=host version=1 -type WebSocketService interface { - // Connect to a WebSocket endpoint - Connect(context.Context, *ConnectRequest) (*ConnectResponse, error) - // Send a text message - SendText(context.Context, *SendTextRequest) (*SendTextResponse, error) - // Send binary data - SendBinary(context.Context, *SendBinaryRequest) (*SendBinaryResponse, error) - // Close a connection - Close(context.Context, *CloseRequest) (*CloseResponse, error) -} diff --git a/plugins/host/websocket/websocket.proto b/plugins/host/websocket/websocket.proto deleted file mode 100644 index 53adaca95..000000000 --- a/plugins/host/websocket/websocket.proto +++ /dev/null @@ -1,57 +0,0 @@ -syntax = "proto3"; -package websocket; -option go_package = "github.com/navidrome/navidrome/plugins/host/websocket"; - -// go:plugin type=host version=1 -service WebSocketService { - // Connect to a WebSocket endpoint - rpc Connect(ConnectRequest) returns (ConnectResponse); - - // Send a text message - rpc SendText(SendTextRequest) returns (SendTextResponse); - - // Send binary data - rpc SendBinary(SendBinaryRequest) returns (SendBinaryResponse); - - // Close a connection - rpc Close(CloseRequest) returns (CloseResponse); -} - -message ConnectRequest { - string url = 1; - map<string, string> headers = 2; - string connection_id = 3; -} - -message ConnectResponse { - string connection_id = 1; - string error = 2; -} - -message SendTextRequest { - string connection_id = 1; - string message = 2; -} - -message SendTextResponse { - string error = 1; -} - -message SendBinaryRequest { - string connection_id = 1; - bytes data = 2; -} - -message SendBinaryResponse { - string error = 1; -} - -message CloseRequest { - string connection_id = 1; - int32 code = 2; - string reason = 3; -} - -message CloseResponse { - string error = 1; -} \ No newline at end of file diff --git a/plugins/host/websocket/websocket_host.pb.go b/plugins/host/websocket/websocket_host.pb.go deleted file mode 100644 index b95eb451c..000000000 --- a/plugins/host/websocket/websocket_host.pb.go +++ /dev/null @@ -1,170 +0,0 @@ -//go:build !wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/websocket/websocket.proto - -package websocket - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - wazero "github.com/tetratelabs/wazero" - api "github.com/tetratelabs/wazero/api" -) - -const ( - i32 = api.ValueTypeI32 - i64 = api.ValueTypeI64 -) - -type _webSocketService struct { - WebSocketService -} - -// Instantiate a Go-defined module named "env" that exports host functions. -func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions WebSocketService) error { - envBuilder := r.NewHostModuleBuilder("env") - h := _webSocketService{hostFunctions} - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Connect), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("connect") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._SendText), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("send_text") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._SendBinary), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("send_binary") - - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(h._Close), []api.ValueType{i32, i32}, []api.ValueType{i64}). - WithParameterNames("offset", "size"). - Export("close") - - _, err := envBuilder.Instantiate(ctx) - return err -} - -// Connect to a WebSocket endpoint - -func (h _webSocketService) _Connect(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(ConnectRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Connect(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Send a text message - -func (h _webSocketService) _SendText(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(SendTextRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.SendText(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Send binary data - -func (h _webSocketService) _SendBinary(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(SendBinaryRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.SendBinary(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} - -// Close a connection - -func (h _webSocketService) _Close(ctx context.Context, m api.Module, stack []uint64) { - offset, size := uint32(stack[0]), uint32(stack[1]) - buf, err := wasm.ReadMemory(m.Memory(), offset, size) - if err != nil { - panic(err) - } - request := new(CloseRequest) - err = request.UnmarshalVT(buf) - if err != nil { - panic(err) - } - resp, err := h.Close(ctx, request) - if err != nil { - panic(err) - } - buf, err = resp.MarshalVT() - if err != nil { - panic(err) - } - ptr, err := wasm.WriteMemory(ctx, m, buf) - if err != nil { - panic(err) - } - ptrLen := (ptr << uint64(32)) | uint64(len(buf)) - stack[0] = ptrLen -} diff --git a/plugins/host/websocket/websocket_plugin.pb.go b/plugins/host/websocket/websocket_plugin.pb.go deleted file mode 100644 index e7d5c3fe0..000000000 --- a/plugins/host/websocket/websocket_plugin.pb.go +++ /dev/null @@ -1,113 +0,0 @@ -//go:build wasip1 - -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/websocket/websocket.proto - -package websocket - -import ( - context "context" - wasm "github.com/knqyf263/go-plugin/wasm" - _ "unsafe" -) - -type webSocketService struct{} - -func NewWebSocketService() WebSocketService { - return webSocketService{} -} - -//go:wasmimport env connect -func _connect(ptr uint32, size uint32) uint64 - -func (h webSocketService) Connect(ctx context.Context, request *ConnectRequest) (*ConnectResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _connect(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(ConnectResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env send_text -func _send_text(ptr uint32, size uint32) uint64 - -func (h webSocketService) SendText(ctx context.Context, request *SendTextRequest) (*SendTextResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _send_text(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(SendTextResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env send_binary -func _send_binary(ptr uint32, size uint32) uint64 - -func (h webSocketService) SendBinary(ctx context.Context, request *SendBinaryRequest) (*SendBinaryResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _send_binary(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(SendBinaryResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} - -//go:wasmimport env close -func _close(ptr uint32, size uint32) uint64 - -func (h webSocketService) Close(ctx context.Context, request *CloseRequest) (*CloseResponse, error) { - buf, err := request.MarshalVT() - if err != nil { - return nil, err - } - ptr, size := wasm.ByteToPtr(buf) - ptrSize := _close(ptr, size) - wasm.Free(ptr) - - ptr = uint32(ptrSize >> 32) - size = uint32(ptrSize) - buf = wasm.PtrToByte(ptr, size) - - response := new(CloseResponse) - if err = response.UnmarshalVT(buf); err != nil { - return nil, err - } - return response, nil -} diff --git a/plugins/host/websocket/websocket_plugin_dev.go b/plugins/host/websocket/websocket_plugin_dev.go deleted file mode 100644 index cfb72462a..000000000 --- a/plugins/host/websocket/websocket_plugin_dev.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !wasip1 - -package websocket - -func NewWebSocketService() WebSocketService { - panic("not implemented") -} diff --git a/plugins/host/websocket/websocket_vtproto.pb.go b/plugins/host/websocket/websocket_vtproto.pb.go deleted file mode 100644 index fb15a22b7..000000000 --- a/plugins/host/websocket/websocket_vtproto.pb.go +++ /dev/null @@ -1,1618 +0,0 @@ -// Code generated by protoc-gen-go-plugin. DO NOT EDIT. -// versions: -// protoc-gen-go-plugin v0.1.0 -// protoc v5.29.3 -// source: host/websocket/websocket.proto - -package websocket - -import ( - fmt "fmt" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - io "io" - bits "math/bits" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -func (m *ConnectRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ConnectRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ConnectRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0x1a - } - if len(m.Headers) > 0 { - for k := range m.Headers { - v := m.Headers[k] - baseI := i - i -= len(v) - copy(dAtA[i:], v) - i = encodeVarint(dAtA, i, uint64(len(v))) - i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = encodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = encodeVarint(dAtA, i, uint64(baseI-i)) - i-- - dAtA[i] = 0x12 - } - } - if len(m.Url) > 0 { - i -= len(m.Url) - copy(dAtA[i:], m.Url) - i = encodeVarint(dAtA, i, uint64(len(m.Url))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *ConnectResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *ConnectResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *ConnectResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0x12 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SendTextRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SendTextRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SendTextRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Message) > 0 { - i -= len(m.Message) - copy(dAtA[i:], m.Message) - i = encodeVarint(dAtA, i, uint64(len(m.Message))) - i-- - dAtA[i] = 0x12 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SendTextResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SendTextResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SendTextResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SendBinaryRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SendBinaryRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SendBinaryRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Data) > 0 { - i -= len(m.Data) - copy(dAtA[i:], m.Data) - i = encodeVarint(dAtA, i, uint64(len(m.Data))) - i-- - dAtA[i] = 0x12 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *SendBinaryResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SendBinaryResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *SendBinaryResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *CloseRequest) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CloseRequest) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *CloseRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Reason) > 0 { - i -= len(m.Reason) - copy(dAtA[i:], m.Reason) - i = encodeVarint(dAtA, i, uint64(len(m.Reason))) - i-- - dAtA[i] = 0x1a - } - if m.Code != 0 { - i = encodeVarint(dAtA, i, uint64(m.Code)) - i-- - dAtA[i] = 0x10 - } - if len(m.ConnectionId) > 0 { - i -= len(m.ConnectionId) - copy(dAtA[i:], m.ConnectionId) - i = encodeVarint(dAtA, i, uint64(len(m.ConnectionId))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *CloseResponse) MarshalVT() (dAtA []byte, err error) { - if m == nil { - return nil, nil - } - size := m.SizeVT() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBufferVT(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CloseResponse) MarshalToVT(dAtA []byte) (int, error) { - size := m.SizeVT() - return m.MarshalToSizedBufferVT(dAtA[:size]) -} - -func (m *CloseResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { - if m == nil { - return 0, nil - } - i := len(dAtA) - _ = i - var l int - _ = l - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) - } - if len(m.Error) > 0 { - i -= len(m.Error) - copy(dAtA[i:], m.Error) - i = encodeVarint(dAtA, i, uint64(len(m.Error))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func encodeVarint(dAtA []byte, offset int, v uint64) int { - offset -= sov(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *ConnectRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Url) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if len(m.Headers) > 0 { - for k, v := range m.Headers { - _ = k - _ = v - mapEntrySize := 1 + len(k) + sov(uint64(len(k))) + 1 + len(v) + sov(uint64(len(v))) - n += mapEntrySize + 1 + sov(uint64(mapEntrySize)) - } - } - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *ConnectResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *SendTextRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Message) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *SendTextResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *SendBinaryRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - l = len(m.Data) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *SendBinaryResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *CloseRequest) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.ConnectionId) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - if m.Code != 0 { - n += 1 + sov(uint64(m.Code)) - } - l = len(m.Reason) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func (m *CloseResponse) SizeVT() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Error) - if l > 0 { - n += 1 + l + sov(uint64(l)) - } - n += len(m.unknownFields) - return n -} - -func sov(x uint64) (n int) { - return (bits.Len64(x|1) + 6) / 7 -} -func soz(x uint64) (n int) { - return sov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *ConnectRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ConnectRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ConnectRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Url = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Headers", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Headers == nil { - m.Headers = make(map[string]string) - } - var mapkey string - var mapvalue string - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var stringLenmapvalue uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapvalue |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapvalue := int(stringLenmapvalue) - if intStringLenmapvalue < 0 { - return ErrInvalidLength - } - postStringIndexmapvalue := iNdEx + intStringLenmapvalue - if postStringIndexmapvalue < 0 { - return ErrInvalidLength - } - if postStringIndexmapvalue > l { - return io.ErrUnexpectedEOF - } - mapvalue = string(dAtA[iNdEx:postStringIndexmapvalue]) - iNdEx = postStringIndexmapvalue - } else { - iNdEx = entryPreIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Headers[mapkey] = mapvalue - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *ConnectResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: ConnectResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: ConnectResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SendTextRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SendTextRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SendTextRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Message = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SendTextResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SendTextResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SendTextResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SendBinaryRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SendBinaryRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SendBinaryRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) - if m.Data == nil { - m.Data = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SendBinaryResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SendBinaryResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SendBinaryResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *CloseRequest) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CloseRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CloseRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ConnectionId", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.ConnectionId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Code", wireType) - } - m.Code = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Code |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Reason", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Reason = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *CloseResponse) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CloseResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CloseResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Error = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} - -func skip(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflow - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLength - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroup - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLength - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLength = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflow = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroup = fmt.Errorf("proto: unexpected end of group") -) diff --git a/plugins/host_artwork.go b/plugins/host_artwork.go deleted file mode 100644 index dac622206..000000000 --- a/plugins/host_artwork.go +++ /dev/null @@ -1,47 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "net/http" - "net/url" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/plugins/host/artwork" - "github.com/navidrome/navidrome/server/public" -) - -type artworkServiceImpl struct{} - -func (a *artworkServiceImpl) GetArtistUrl(_ context.Context, req *artwork.GetArtworkUrlRequest) (*artwork.GetArtworkUrlResponse, error) { - artID := model.ArtworkID{Kind: model.KindArtistArtwork, ID: req.Id} - imageURL := public.ImageURL(a.createRequest(), artID, int(req.Size)) - return &artwork.GetArtworkUrlResponse{Url: imageURL}, nil -} - -func (a *artworkServiceImpl) GetAlbumUrl(_ context.Context, req *artwork.GetArtworkUrlRequest) (*artwork.GetArtworkUrlResponse, error) { - artID := model.ArtworkID{Kind: model.KindAlbumArtwork, ID: req.Id} - imageURL := public.ImageURL(a.createRequest(), artID, int(req.Size)) - return &artwork.GetArtworkUrlResponse{Url: imageURL}, nil -} - -func (a *artworkServiceImpl) GetTrackUrl(_ context.Context, req *artwork.GetArtworkUrlRequest) (*artwork.GetArtworkUrlResponse, error) { - artID := model.ArtworkID{Kind: model.KindMediaFileArtwork, ID: req.Id} - imageURL := public.ImageURL(a.createRequest(), artID, int(req.Size)) - return &artwork.GetArtworkUrlResponse{Url: imageURL}, nil -} - -func (a *artworkServiceImpl) createRequest() *http.Request { - var scheme, host string - if conf.Server.ShareURL != "" { - shareURL, _ := url.Parse(conf.Server.ShareURL) - scheme = shareURL.Scheme - host = shareURL.Host - } else { - scheme = "http" - host = "localhost" - } - r, _ := http.NewRequest("GET", fmt.Sprintf("%s://%s", scheme, host), nil) - return r -} diff --git a/plugins/host_artwork_test.go b/plugins/host_artwork_test.go deleted file mode 100644 index b6667bde3..000000000 --- a/plugins/host_artwork_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package plugins - -import ( - "context" - - "github.com/go-chi/jwtauth/v5" - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/conf/configtest" - "github.com/navidrome/navidrome/core/auth" - "github.com/navidrome/navidrome/plugins/host/artwork" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("ArtworkService", func() { - var svc *artworkServiceImpl - - BeforeEach(func() { - DeferCleanup(configtest.SetupConfig()) - // Setup auth for tests - auth.TokenAuth = jwtauth.New("HS256", []byte("super secret"), nil) - svc = &artworkServiceImpl{} - }) - - Context("with ShareURL configured", func() { - BeforeEach(func() { - conf.Server.ShareURL = "https://music.example.com" - }) - - It("returns artist artwork URL", func() { - resp, err := svc.GetArtistUrl(context.Background(), &artwork.GetArtworkUrlRequest{Id: "123", Size: 300}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Url).To(ContainSubstring("https://music.example.com")) - Expect(resp.Url).To(ContainSubstring("size=300")) - }) - - It("returns album artwork URL", func() { - resp, err := svc.GetAlbumUrl(context.Background(), &artwork.GetArtworkUrlRequest{Id: "456"}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Url).To(ContainSubstring("https://music.example.com")) - }) - - It("returns track artwork URL", func() { - resp, err := svc.GetTrackUrl(context.Background(), &artwork.GetArtworkUrlRequest{Id: "789", Size: 150}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Url).To(ContainSubstring("https://music.example.com")) - Expect(resp.Url).To(ContainSubstring("size=150")) - }) - }) - - Context("without ShareURL configured", func() { - It("returns localhost URLs", func() { - resp, err := svc.GetArtistUrl(context.Background(), &artwork.GetArtworkUrlRequest{Id: "123"}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Url).To(ContainSubstring("http://localhost")) - }) - }) -}) diff --git a/plugins/host_cache.go b/plugins/host_cache.go deleted file mode 100644 index 291a17870..000000000 --- a/plugins/host_cache.go +++ /dev/null @@ -1,152 +0,0 @@ -package plugins - -import ( - "context" - "sync" - "time" - - "github.com/jellydator/ttlcache/v3" - "github.com/navidrome/navidrome/log" - cacheproto "github.com/navidrome/navidrome/plugins/host/cache" -) - -const ( - defaultCacheTTL = 24 * time.Hour -) - -// cacheServiceImpl implements the cache.CacheService interface -type cacheServiceImpl struct { - pluginID string - defaultTTL time.Duration -} - -var ( - _cache *ttlcache.Cache[string, any] - initCacheOnce sync.Once -) - -// newCacheService creates a new cacheServiceImpl instance -func newCacheService(pluginID string) *cacheServiceImpl { - initCacheOnce.Do(func() { - opts := []ttlcache.Option[string, any]{ - ttlcache.WithTTL[string, any](defaultCacheTTL), - } - _cache = ttlcache.New[string, any](opts...) - - // Start the janitor goroutine to clean up expired entries - go _cache.Start() - }) - - return &cacheServiceImpl{ - pluginID: pluginID, - defaultTTL: defaultCacheTTL, - } -} - -// mapKey combines the plugin name and a provided key to create a unique cache key. -func (s *cacheServiceImpl) mapKey(key string) string { - return s.pluginID + ":" + key -} - -// getTTL converts seconds to a duration, using default if 0 -func (s *cacheServiceImpl) getTTL(seconds int64) time.Duration { - if seconds <= 0 { - return s.defaultTTL - } - return time.Duration(seconds) * time.Second -} - -// setCacheValue is a generic function to set a value in the cache -func setCacheValue[T any](ctx context.Context, cs *cacheServiceImpl, key string, value T, ttlSeconds int64) (*cacheproto.SetResponse, error) { - ttl := cs.getTTL(ttlSeconds) - key = cs.mapKey(key) - _cache.Set(key, value, ttl) - return &cacheproto.SetResponse{Success: true}, nil -} - -// getCacheValue is a generic function to get a value from the cache -func getCacheValue[T any](ctx context.Context, cs *cacheServiceImpl, key string, typeName string) (T, bool, error) { - key = cs.mapKey(key) - var zero T - item := _cache.Get(key) - if item == nil { - return zero, false, nil - } - - value, ok := item.Value().(T) - if !ok { - log.Debug(ctx, "Type mismatch in cache", "plugin", cs.pluginID, "key", key, "expected", typeName) - return zero, false, nil - } - return value, true, nil -} - -// SetString sets a string value in the cache -func (s *cacheServiceImpl) SetString(ctx context.Context, req *cacheproto.SetStringRequest) (*cacheproto.SetResponse, error) { - return setCacheValue(ctx, s, req.Key, req.Value, req.TtlSeconds) -} - -// GetString gets a string value from the cache -func (s *cacheServiceImpl) GetString(ctx context.Context, req *cacheproto.GetRequest) (*cacheproto.GetStringResponse, error) { - value, exists, err := getCacheValue[string](ctx, s, req.Key, "string") - if err != nil { - return nil, err - } - return &cacheproto.GetStringResponse{Exists: exists, Value: value}, nil -} - -// SetInt sets an integer value in the cache -func (s *cacheServiceImpl) SetInt(ctx context.Context, req *cacheproto.SetIntRequest) (*cacheproto.SetResponse, error) { - return setCacheValue(ctx, s, req.Key, req.Value, req.TtlSeconds) -} - -// GetInt gets an integer value from the cache -func (s *cacheServiceImpl) GetInt(ctx context.Context, req *cacheproto.GetRequest) (*cacheproto.GetIntResponse, error) { - value, exists, err := getCacheValue[int64](ctx, s, req.Key, "int64") - if err != nil { - return nil, err - } - return &cacheproto.GetIntResponse{Exists: exists, Value: value}, nil -} - -// SetFloat sets a float value in the cache -func (s *cacheServiceImpl) SetFloat(ctx context.Context, req *cacheproto.SetFloatRequest) (*cacheproto.SetResponse, error) { - return setCacheValue(ctx, s, req.Key, req.Value, req.TtlSeconds) -} - -// GetFloat gets a float value from the cache -func (s *cacheServiceImpl) GetFloat(ctx context.Context, req *cacheproto.GetRequest) (*cacheproto.GetFloatResponse, error) { - value, exists, err := getCacheValue[float64](ctx, s, req.Key, "float64") - if err != nil { - return nil, err - } - return &cacheproto.GetFloatResponse{Exists: exists, Value: value}, nil -} - -// SetBytes sets a byte slice value in the cache -func (s *cacheServiceImpl) SetBytes(ctx context.Context, req *cacheproto.SetBytesRequest) (*cacheproto.SetResponse, error) { - return setCacheValue(ctx, s, req.Key, req.Value, req.TtlSeconds) -} - -// GetBytes gets a byte slice value from the cache -func (s *cacheServiceImpl) GetBytes(ctx context.Context, req *cacheproto.GetRequest) (*cacheproto.GetBytesResponse, error) { - value, exists, err := getCacheValue[[]byte](ctx, s, req.Key, "[]byte") - if err != nil { - return nil, err - } - return &cacheproto.GetBytesResponse{Exists: exists, Value: value}, nil -} - -// Remove removes a value from the cache -func (s *cacheServiceImpl) Remove(ctx context.Context, req *cacheproto.RemoveRequest) (*cacheproto.RemoveResponse, error) { - key := s.mapKey(req.Key) - _cache.Delete(key) - return &cacheproto.RemoveResponse{Success: true}, nil -} - -// Has checks if a key exists in the cache -func (s *cacheServiceImpl) Has(ctx context.Context, req *cacheproto.HasRequest) (*cacheproto.HasResponse, error) { - key := s.mapKey(req.Key) - item := _cache.Get(key) - return &cacheproto.HasResponse{Exists: item != nil}, nil -} diff --git a/plugins/host_cache_test.go b/plugins/host_cache_test.go deleted file mode 100644 index efb03e289..000000000 --- a/plugins/host_cache_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package plugins - -import ( - "context" - "time" - - "github.com/navidrome/navidrome/plugins/host/cache" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("CacheService", func() { - var service *cacheServiceImpl - var ctx context.Context - - BeforeEach(func() { - ctx = context.Background() - service = newCacheService("test_plugin") - }) - - Describe("getTTL", func() { - It("returns default TTL when seconds is 0", func() { - ttl := service.getTTL(0) - Expect(ttl).To(Equal(defaultCacheTTL)) - }) - - It("returns default TTL when seconds is negative", func() { - ttl := service.getTTL(-10) - Expect(ttl).To(Equal(defaultCacheTTL)) - }) - - It("returns correct duration when seconds is positive", func() { - ttl := service.getTTL(60) - Expect(ttl).To(Equal(time.Minute)) - }) - }) - - Describe("String Operations", func() { - It("sets and gets a string value", func() { - _, err := service.SetString(ctx, &cache.SetStringRequest{ - Key: "string_key", - Value: "test_value", - TtlSeconds: 300, - }) - Expect(err).NotTo(HaveOccurred()) - - res, err := service.GetString(ctx, &cache.GetRequest{Key: "string_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeTrue()) - Expect(res.Value).To(Equal("test_value")) - }) - - It("returns not exists for missing key", func() { - res, err := service.GetString(ctx, &cache.GetRequest{Key: "missing_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeFalse()) - }) - }) - - Describe("Integer Operations", func() { - It("sets and gets an integer value", func() { - _, err := service.SetInt(ctx, &cache.SetIntRequest{ - Key: "int_key", - Value: 42, - TtlSeconds: 300, - }) - Expect(err).NotTo(HaveOccurred()) - - res, err := service.GetInt(ctx, &cache.GetRequest{Key: "int_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeTrue()) - Expect(res.Value).To(Equal(int64(42))) - }) - }) - - Describe("Float Operations", func() { - It("sets and gets a float value", func() { - _, err := service.SetFloat(ctx, &cache.SetFloatRequest{ - Key: "float_key", - Value: 3.14, - TtlSeconds: 300, - }) - Expect(err).NotTo(HaveOccurred()) - - res, err := service.GetFloat(ctx, &cache.GetRequest{Key: "float_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeTrue()) - Expect(res.Value).To(Equal(3.14)) - }) - }) - - Describe("Bytes Operations", func() { - It("sets and gets a bytes value", func() { - byteData := []byte("hello world") - _, err := service.SetBytes(ctx, &cache.SetBytesRequest{ - Key: "bytes_key", - Value: byteData, - TtlSeconds: 300, - }) - Expect(err).NotTo(HaveOccurred()) - - res, err := service.GetBytes(ctx, &cache.GetRequest{Key: "bytes_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeTrue()) - Expect(res.Value).To(Equal(byteData)) - }) - }) - - Describe("Type mismatch handling", func() { - It("returns not exists when type doesn't match the getter", func() { - // Set string - _, err := service.SetString(ctx, &cache.SetStringRequest{ - Key: "mixed_key", - Value: "string value", - }) - Expect(err).NotTo(HaveOccurred()) - - // Try to get as int - res, err := service.GetInt(ctx, &cache.GetRequest{Key: "mixed_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeFalse()) - }) - }) - - Describe("Remove Operation", func() { - It("removes a value from the cache", func() { - // Set a value - _, err := service.SetString(ctx, &cache.SetStringRequest{ - Key: "remove_key", - Value: "to be removed", - }) - Expect(err).NotTo(HaveOccurred()) - - // Verify it exists - res, err := service.Has(ctx, &cache.HasRequest{Key: "remove_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeTrue()) - - // Remove it - _, err = service.Remove(ctx, &cache.RemoveRequest{Key: "remove_key"}) - Expect(err).NotTo(HaveOccurred()) - - // Verify it's gone - res, err = service.Has(ctx, &cache.HasRequest{Key: "remove_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeFalse()) - }) - }) - - Describe("Has Operation", func() { - It("returns true for existing key", func() { - // Set a value - _, err := service.SetString(ctx, &cache.SetStringRequest{ - Key: "existing_key", - Value: "exists", - }) - Expect(err).NotTo(HaveOccurred()) - - // Check if it exists - res, err := service.Has(ctx, &cache.HasRequest{Key: "existing_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeTrue()) - }) - - It("returns false for non-existing key", func() { - res, err := service.Has(ctx, &cache.HasRequest{Key: "non_existing_key"}) - Expect(err).NotTo(HaveOccurred()) - Expect(res.Exists).To(BeFalse()) - }) - }) -}) diff --git a/plugins/host_config.go b/plugins/host_config.go deleted file mode 100644 index baee6a00c..000000000 --- a/plugins/host_config.go +++ /dev/null @@ -1,22 +0,0 @@ -package plugins - -import ( - "context" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/plugins/host/config" -) - -type configServiceImpl struct { - pluginID string -} - -func (c *configServiceImpl) GetPluginConfig(ctx context.Context, req *config.GetPluginConfigRequest) (*config.GetPluginConfigResponse, error) { - cfg, ok := conf.Server.PluginConfig[c.pluginID] - if !ok { - cfg = map[string]string{} - } - return &config.GetPluginConfigResponse{ - Config: cfg, - }, nil -} diff --git a/plugins/host_config_test.go b/plugins/host_config_test.go deleted file mode 100644 index bae7043be..000000000 --- a/plugins/host_config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package plugins - -import ( - "context" - - "github.com/navidrome/navidrome/conf" - hostconfig "github.com/navidrome/navidrome/plugins/host/config" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("configServiceImpl", func() { - var ( - svc *configServiceImpl - pluginName string - ) - - BeforeEach(func() { - pluginName = "testplugin" - svc = &configServiceImpl{pluginID: pluginName} - conf.Server.PluginConfig = map[string]map[string]string{ - pluginName: {"foo": "bar", "baz": "qux"}, - } - }) - - It("returns config for known plugin", func() { - resp, err := svc.GetPluginConfig(context.Background(), &hostconfig.GetPluginConfigRequest{}) - Expect(err).To(BeNil()) - Expect(resp.Config).To(HaveKeyWithValue("foo", "bar")) - Expect(resp.Config).To(HaveKeyWithValue("baz", "qux")) - }) - - It("returns error for unknown plugin", func() { - svc.pluginID = "unknown" - resp, err := svc.GetPluginConfig(context.Background(), &hostconfig.GetPluginConfigRequest{}) - Expect(err).To(BeNil()) - Expect(resp.Config).To(BeEmpty()) - }) - - It("returns empty config if plugin config is empty", func() { - conf.Server.PluginConfig[pluginName] = map[string]string{} - resp, err := svc.GetPluginConfig(context.Background(), &hostconfig.GetPluginConfigRequest{}) - Expect(err).To(BeNil()) - Expect(resp.Config).To(BeEmpty()) - }) -}) diff --git a/plugins/host_http.go b/plugins/host_http.go deleted file mode 100644 index 24fc77b18..000000000 --- a/plugins/host_http.go +++ /dev/null @@ -1,114 +0,0 @@ -package plugins - -import ( - "bytes" - "cmp" - "context" - "io" - "net/http" - "time" - - "github.com/navidrome/navidrome/log" - hosthttp "github.com/navidrome/navidrome/plugins/host/http" -) - -type httpServiceImpl struct { - pluginID string - permissions *httpPermissions -} - -const defaultTimeout = 10 * time.Second - -func (s *httpServiceImpl) Get(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodGet, req) -} - -func (s *httpServiceImpl) Post(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodPost, req) -} - -func (s *httpServiceImpl) Put(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodPut, req) -} - -func (s *httpServiceImpl) Delete(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodDelete, req) -} - -func (s *httpServiceImpl) Patch(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodPatch, req) -} - -func (s *httpServiceImpl) Head(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodHead, req) -} - -func (s *httpServiceImpl) Options(ctx context.Context, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - return s.doHttp(ctx, http.MethodOptions, req) -} - -func (s *httpServiceImpl) doHttp(ctx context.Context, method string, req *hosthttp.HttpRequest) (*hosthttp.HttpResponse, error) { - // Check permissions if they exist - if s.permissions != nil { - if err := s.permissions.IsRequestAllowed(req.Url, method); err != nil { - log.Warn(ctx, "HTTP request blocked by permissions", "plugin", s.pluginID, "url", req.Url, "method", method, err) - return &hosthttp.HttpResponse{Error: "Request blocked by plugin permissions: " + err.Error()}, nil - } - } - client := &http.Client{ - Timeout: cmp.Or(time.Duration(req.TimeoutMs)*time.Millisecond, defaultTimeout), - } - - // Configure redirect policy based on permissions - if s.permissions != nil { - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - // Enforce maximum redirect limit - if len(via) >= httpMaxRedirects { - log.Warn(ctx, "HTTP redirect limit exceeded", "plugin", s.pluginID, "url", req.URL.String(), "redirectCount", len(via)) - return http.ErrUseLastResponse - } - - // Check if redirect destination is allowed - if err := s.permissions.IsRequestAllowed(req.URL.String(), req.Method); err != nil { - log.Warn(ctx, "HTTP redirect blocked by permissions", "plugin", s.pluginID, "url", req.URL.String(), "method", req.Method, err) - return http.ErrUseLastResponse - } - - return nil // Allow redirect - } - } - var body io.Reader - if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch { - body = bytes.NewReader(req.Body) - } - httpReq, err := http.NewRequestWithContext(ctx, method, req.Url, body) - if err != nil { - return nil, err - } - for k, v := range req.Headers { - httpReq.Header.Set(k, v) - } - resp, err := client.Do(httpReq) - if err != nil { - log.Trace(ctx, "HttpService request error", "method", method, "url", req.Url, "headers", req.Headers, err) - return &hosthttp.HttpResponse{Error: err.Error()}, nil - } - log.Trace(ctx, "HttpService request", "method", method, "url", req.Url, "headers", req.Headers, "resp.status", resp.StatusCode) - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - log.Trace(ctx, "HttpService request error", "method", method, "url", req.Url, "headers", req.Headers, "resp.status", resp.StatusCode, err) - return &hosthttp.HttpResponse{Error: err.Error()}, nil - } - headers := map[string]string{} - for k, v := range resp.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } - return &hosthttp.HttpResponse{ - Status: int32(resp.StatusCode), - Body: respBody, - Headers: headers, - }, nil -} diff --git a/plugins/host_http_permissions.go b/plugins/host_http_permissions.go deleted file mode 100644 index 158bdb105..000000000 --- a/plugins/host_http_permissions.go +++ /dev/null @@ -1,90 +0,0 @@ -package plugins - -import ( - "fmt" - "strings" - - "github.com/navidrome/navidrome/plugins/schema" -) - -// Maximum number of HTTP redirects allowed for plugin requests -const httpMaxRedirects = 5 - -// HTTPPermissions represents granular HTTP access permissions for plugins -type httpPermissions struct { - *networkPermissionsBase - AllowedUrls map[string][]string `json:"allowedUrls"` - matcher *urlMatcher -} - -// parseHTTPPermissions extracts HTTP permissions from the schema -func parseHTTPPermissions(permData *schema.PluginManifestPermissionsHttp) (*httpPermissions, error) { - base := &networkPermissionsBase{ - AllowLocalNetwork: permData.AllowLocalNetwork, - } - - if len(permData.AllowedUrls) == 0 { - return nil, fmt.Errorf("allowedUrls must contain at least one URL pattern") - } - - allowedUrls := make(map[string][]string) - for urlPattern, methodEnums := range permData.AllowedUrls { - methods := make([]string, len(methodEnums)) - for i, methodEnum := range methodEnums { - methods[i] = string(methodEnum) - } - allowedUrls[urlPattern] = methods - } - - return &httpPermissions{ - networkPermissionsBase: base, - AllowedUrls: allowedUrls, - matcher: newURLMatcher(), - }, nil -} - -// IsRequestAllowed checks if a specific network request is allowed by the permissions -func (p *httpPermissions) IsRequestAllowed(requestURL, operation string) error { - if _, err := checkURLPolicy(requestURL, p.AllowLocalNetwork); err != nil { - return err - } - - // allowedUrls is now required - no fallback to allow all URLs - if p.AllowedUrls == nil || len(p.AllowedUrls) == 0 { - return fmt.Errorf("no allowed URLs configured for plugin") - } - - matcher := newURLMatcher() - - // Check URL patterns and operations - // First try exact matches, then wildcard matches - operation = strings.ToUpper(operation) - - // Phase 1: Check for exact matches first - for urlPattern, allowedOperations := range p.AllowedUrls { - if !strings.Contains(urlPattern, "*") && matcher.MatchesURLPattern(requestURL, urlPattern) { - // Check if operation is allowed - for _, allowedOperation := range allowedOperations { - if allowedOperation == "*" || allowedOperation == operation { - return nil - } - } - return fmt.Errorf("operation %s not allowed for URL pattern %s", operation, urlPattern) - } - } - - // Phase 2: Check wildcard patterns - for urlPattern, allowedOperations := range p.AllowedUrls { - if strings.Contains(urlPattern, "*") && matcher.MatchesURLPattern(requestURL, urlPattern) { - // Check if operation is allowed - for _, allowedOperation := range allowedOperations { - if allowedOperation == "*" || allowedOperation == operation { - return nil - } - } - return fmt.Errorf("operation %s not allowed for URL pattern %s", operation, urlPattern) - } - } - - return fmt.Errorf("URL %s does not match any allowed URL patterns", requestURL) -} diff --git a/plugins/host_http_permissions_test.go b/plugins/host_http_permissions_test.go deleted file mode 100644 index 3385ffc03..000000000 --- a/plugins/host_http_permissions_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package plugins - -import ( - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("HTTP Permissions", func() { - Describe("parseHTTPPermissions", func() { - It("should parse valid HTTP permissions", func() { - permData := &schema.PluginManifestPermissionsHttp{ - Reason: "Need to fetch album artwork", - AllowLocalNetwork: false, - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "https://api.example.com/*": { - schema.PluginManifestPermissionsHttpAllowedUrlsValueElemGET, - schema.PluginManifestPermissionsHttpAllowedUrlsValueElemPOST, - }, - "https://cdn.example.com/*": { - schema.PluginManifestPermissionsHttpAllowedUrlsValueElemGET, - }, - }, - } - - perms, err := parseHTTPPermissions(permData) - Expect(err).To(BeNil()) - Expect(perms).ToNot(BeNil()) - Expect(perms.AllowLocalNetwork).To(BeFalse()) - Expect(perms.AllowedUrls).To(HaveLen(2)) - Expect(perms.AllowedUrls["https://api.example.com/*"]).To(Equal([]string{"GET", "POST"})) - Expect(perms.AllowedUrls["https://cdn.example.com/*"]).To(Equal([]string{"GET"})) - }) - - It("should fail if allowedUrls is empty", func() { - permData := &schema.PluginManifestPermissionsHttp{ - Reason: "Need to fetch album artwork", - AllowLocalNetwork: false, - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{}, - } - - _, err := parseHTTPPermissions(permData) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("allowedUrls must contain at least one URL pattern")) - }) - - It("should handle method enum types correctly", func() { - permData := &schema.PluginManifestPermissionsHttp{ - Reason: "Need to fetch album artwork", - AllowLocalNetwork: false, - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "https://api.example.com/*": { - schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard, // "*" - }, - }, - } - - perms, err := parseHTTPPermissions(permData) - Expect(err).To(BeNil()) - Expect(perms.AllowedUrls["https://api.example.com/*"]).To(Equal([]string{"*"})) - }) - }) - - Describe("IsRequestAllowed", func() { - var perms *httpPermissions - - Context("HTTP method-specific validation", func() { - BeforeEach(func() { - perms = &httpPermissions{ - networkPermissionsBase: &networkPermissionsBase{ - Reason: "Test permissions", - AllowLocalNetwork: false, - }, - AllowedUrls: map[string][]string{ - "https://api.example.com": {"GET", "POST"}, - "https://upload.example.com": {"PUT", "PATCH"}, - "https://admin.example.com": {"DELETE"}, - "https://webhook.example.com": {"*"}, - }, - matcher: newURLMatcher(), - } - }) - - DescribeTable("method-specific access control", - func(url, method string, shouldSucceed bool) { - err := perms.IsRequestAllowed(url, method) - if shouldSucceed { - Expect(err).ToNot(HaveOccurred()) - } else { - Expect(err).To(HaveOccurred()) - } - }, - // Allowed methods - Entry("GET to api", "https://api.example.com", "GET", true), - Entry("POST to api", "https://api.example.com", "POST", true), - Entry("PUT to upload", "https://upload.example.com", "PUT", true), - Entry("PATCH to upload", "https://upload.example.com", "PATCH", true), - Entry("DELETE to admin", "https://admin.example.com", "DELETE", true), - Entry("any method to webhook", "https://webhook.example.com", "OPTIONS", true), - Entry("any method to webhook", "https://webhook.example.com", "HEAD", true), - - // Disallowed methods - Entry("DELETE to api", "https://api.example.com", "DELETE", false), - Entry("GET to upload", "https://upload.example.com", "GET", false), - Entry("POST to admin", "https://admin.example.com", "POST", false), - ) - }) - - Context("case insensitive method handling", func() { - BeforeEach(func() { - perms = &httpPermissions{ - networkPermissionsBase: &networkPermissionsBase{ - Reason: "Test permissions", - AllowLocalNetwork: false, - }, - AllowedUrls: map[string][]string{ - "https://api.example.com": {"GET", "POST"}, // Both uppercase for consistency - }, - matcher: newURLMatcher(), - } - }) - - DescribeTable("case insensitive method matching", - func(method string, shouldSucceed bool) { - err := perms.IsRequestAllowed("https://api.example.com", method) - if shouldSucceed { - Expect(err).ToNot(HaveOccurred()) - } else { - Expect(err).To(HaveOccurred()) - } - }, - Entry("uppercase GET", "GET", true), - Entry("lowercase get", "get", true), - Entry("mixed case Get", "Get", true), - Entry("uppercase POST", "POST", true), - Entry("lowercase post", "post", true), - Entry("mixed case Post", "Post", true), - Entry("disallowed method", "DELETE", false), - ) - }) - - Context("with complex URL patterns and HTTP methods", func() { - BeforeEach(func() { - perms = &httpPermissions{ - networkPermissionsBase: &networkPermissionsBase{ - Reason: "Test permissions", - AllowLocalNetwork: false, - }, - AllowedUrls: map[string][]string{ - "https://api.example.com/v1/*": {"GET"}, - "https://api.example.com/v1/users": {"POST", "PUT"}, - "https://*.example.com/public/*": {"GET", "HEAD"}, - "https://admin.*.example.com": {"*"}, - }, - matcher: newURLMatcher(), - } - }) - - DescribeTable("complex pattern and method combinations", - func(url, method string, shouldSucceed bool) { - err := perms.IsRequestAllowed(url, method) - if shouldSucceed { - Expect(err).ToNot(HaveOccurred()) - } else { - Expect(err).To(HaveOccurred()) - } - }, - // Path wildcards with specific methods - Entry("GET to v1 path", "https://api.example.com/v1/posts", "GET", true), - Entry("POST to v1 path", "https://api.example.com/v1/posts", "POST", false), - Entry("POST to specific users endpoint", "https://api.example.com/v1/users", "POST", true), - Entry("PUT to specific users endpoint", "https://api.example.com/v1/users", "PUT", true), - Entry("DELETE to specific users endpoint", "https://api.example.com/v1/users", "DELETE", false), - - // Subdomain wildcards with specific methods - Entry("GET to public path on subdomain", "https://cdn.example.com/public/assets", "GET", true), - Entry("HEAD to public path on subdomain", "https://static.example.com/public/files", "HEAD", true), - Entry("POST to public path on subdomain", "https://api.example.com/public/upload", "POST", false), - - // Admin subdomain with all methods - Entry("GET to admin subdomain", "https://admin.prod.example.com", "GET", true), - Entry("POST to admin subdomain", "https://admin.staging.example.com", "POST", true), - Entry("DELETE to admin subdomain", "https://admin.dev.example.com", "DELETE", true), - ) - }) - }) -}) diff --git a/plugins/host_http_test.go b/plugins/host_http_test.go deleted file mode 100644 index b6f823a07..000000000 --- a/plugins/host_http_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package plugins - -import ( - "context" - "net/http" - "net/http/httptest" - "time" - - hosthttp "github.com/navidrome/navidrome/plugins/host/http" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("httpServiceImpl", func() { - var ( - svc *httpServiceImpl - ts *httptest.Server - ) - - BeforeEach(func() { - svc = &httpServiceImpl{} - }) - - AfterEach(func() { - if ts != nil { - ts.Close() - } - }) - - It("should handle GET requests", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Test", "ok") - w.WriteHeader(201) - _, _ = w.Write([]byte("hello")) - })) - resp, err := svc.Get(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - Headers: map[string]string{"A": "B"}, - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(resp.Status).To(Equal(int32(201))) - Expect(string(resp.Body)).To(Equal("hello")) - Expect(resp.Headers["X-Test"]).To(Equal("ok")) - }) - - It("should handle POST requests with body", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b := make([]byte, r.ContentLength) - _, _ = r.Body.Read(b) - _, _ = w.Write([]byte("got:" + string(b))) - })) - resp, err := svc.Post(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - Body: []byte("abc"), - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(string(resp.Body)).To(Equal("got:abc")) - }) - - It("should handle PUT requests with body", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b := make([]byte, r.ContentLength) - _, _ = r.Body.Read(b) - _, _ = w.Write([]byte("put:" + string(b))) - })) - resp, err := svc.Put(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - Body: []byte("xyz"), - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(string(resp.Body)).To(Equal("put:xyz")) - }) - - It("should handle DELETE requests", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(204) - })) - resp, err := svc.Delete(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(resp.Status).To(Equal(int32(204))) - }) - - It("should handle PATCH requests with body", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b := make([]byte, r.ContentLength) - _, _ = r.Body.Read(b) - _, _ = w.Write([]byte("patch:" + string(b))) - })) - resp, err := svc.Patch(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - Body: []byte("test-patch"), - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(string(resp.Body)).To(Equal("patch:test-patch")) - }) - - It("should handle HEAD requests", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", "42") - w.WriteHeader(200) - // HEAD responses shouldn't have a body, but the headers should be present - })) - resp, err := svc.Head(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(resp.Status).To(Equal(int32(200))) - Expect(resp.Headers["Content-Type"]).To(Equal("application/json")) - Expect(resp.Headers["Content-Length"]).To(Equal("42")) - Expect(resp.Body).To(BeEmpty()) // HEAD responses have no body - }) - - It("should handle OPTIONS requests", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Allow", "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS") - w.WriteHeader(200) - })) - resp, err := svc.Options(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(resp.Status).To(Equal(int32(200))) - Expect(resp.Headers["Allow"]).To(Equal("GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS")) - Expect(resp.Headers["Access-Control-Allow-Methods"]).To(Equal("GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS")) - }) - - It("should handle timeouts and errors", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(50 * time.Millisecond) - })) - resp, err := svc.Get(context.Background(), &hosthttp.HttpRequest{ - Url: ts.URL, - TimeoutMs: 1, - }) - Expect(err).To(BeNil()) - Expect(resp).NotTo(BeNil()) - Expect(resp.Error).To(ContainSubstring("deadline exceeded")) - }) - - It("should return error on context timeout", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(50 * time.Millisecond) - })) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) - defer cancel() - resp, err := svc.Get(ctx, &hosthttp.HttpRequest{ - Url: ts.URL, - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp).NotTo(BeNil()) - Expect(resp.Error).To(ContainSubstring("context deadline exceeded")) - }) - - It("should return error on context cancellation", func() { - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(50 * time.Millisecond) - })) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - time.Sleep(1 * time.Millisecond) - cancel() - }() - resp, err := svc.Get(ctx, &hosthttp.HttpRequest{ - Url: ts.URL, - TimeoutMs: 1000, - }) - Expect(err).To(BeNil()) - Expect(resp).NotTo(BeNil()) - Expect(resp.Error).To(ContainSubstring("context canceled")) - }) -}) diff --git a/plugins/host_network_permissions_base.go b/plugins/host_network_permissions_base.go deleted file mode 100644 index c3224fe2a..000000000 --- a/plugins/host_network_permissions_base.go +++ /dev/null @@ -1,192 +0,0 @@ -package plugins - -import ( - "fmt" - "net" - "net/url" - "regexp" - "strings" -) - -// NetworkPermissionsBase contains common functionality for network-based permissions -type networkPermissionsBase struct { - Reason string `json:"reason"` - AllowLocalNetwork bool `json:"allowLocalNetwork,omitempty"` -} - -// URLMatcher provides URL pattern matching functionality -type urlMatcher struct{} - -// newURLMatcher creates a new URL matcher instance -func newURLMatcher() *urlMatcher { - return &urlMatcher{} -} - -// checkURLPolicy performs common checks for a URL against network policies. -func checkURLPolicy(requestURL string, allowLocalNetwork bool) (*url.URL, error) { - parsedURL, err := url.Parse(requestURL) - if err != nil { - return nil, fmt.Errorf("invalid URL: %w", err) - } - - // Check local network restrictions - if !allowLocalNetwork { - if err := checkLocalNetwork(parsedURL); err != nil { - return nil, err - } - } - return parsedURL, nil -} - -// MatchesURLPattern checks if a URL matches a given pattern -func (m *urlMatcher) MatchesURLPattern(requestURL, pattern string) bool { - // Handle wildcard pattern - if pattern == "*" { - return true - } - - // Parse both URLs to handle path matching correctly - reqURL, err := url.Parse(requestURL) - if err != nil { - return false - } - - patternURL, err := url.Parse(pattern) - if err != nil { - // If pattern is not a valid URL, treat it as a simple string pattern - regexPattern := m.urlPatternToRegex(pattern) - matched, err := regexp.MatchString(regexPattern, requestURL) - if err != nil { - return false - } - return matched - } - - // Match scheme - if patternURL.Scheme != "" && patternURL.Scheme != reqURL.Scheme { - return false - } - - // Match host with wildcard support - if !m.matchesHost(reqURL.Host, patternURL.Host) { - return false - } - - // Match path with wildcard support - // Special case: if pattern URL has empty path and contains wildcards, allow any path (domain-only wildcard matching) - if (patternURL.Path == "" || patternURL.Path == "/") && strings.Contains(pattern, "*") { - // This is a domain-only wildcard pattern, allow any path - return true - } - if !m.matchesPath(reqURL.Path, patternURL.Path) { - return false - } - - return true -} - -// urlPatternToRegex converts a URL pattern with wildcards to a regex pattern -func (m *urlMatcher) urlPatternToRegex(pattern string) string { - // Escape special regex characters except * - escaped := regexp.QuoteMeta(pattern) - - // Replace escaped \* with regex pattern for wildcard matching - // For subdomain: *.example.com -> [^.]*\.example\.com - // For path: /api/* -> /api/.* - escaped = strings.ReplaceAll(escaped, "\\*", ".*") - - // Anchor the pattern to match the full URL - return "^" + escaped + "$" -} - -// matchesHost checks if a host matches a pattern with wildcard support -func (m *urlMatcher) matchesHost(host, pattern string) bool { - if pattern == "" { - return true - } - - if pattern == "*" { - return true - } - - // Handle wildcard patterns anywhere in the host - if strings.Contains(pattern, "*") { - patterns := []string{ - strings.ReplaceAll(regexp.QuoteMeta(pattern), "\\*", "[0-9.]+"), // IP pattern - strings.ReplaceAll(regexp.QuoteMeta(pattern), "\\*", "[^.]*"), // Domain pattern - } - - for _, regexPattern := range patterns { - fullPattern := "^" + regexPattern + "$" - if matched, err := regexp.MatchString(fullPattern, host); err == nil && matched { - return true - } - } - return false - } - - return host == pattern -} - -// matchesPath checks if a path matches a pattern with wildcard support -func (m *urlMatcher) matchesPath(path, pattern string) bool { - // Normalize empty paths to "/" - if path == "" { - path = "/" - } - if pattern == "" { - pattern = "/" - } - - if pattern == "*" { - return true - } - - // Handle wildcard paths - if strings.HasSuffix(pattern, "/*") { - prefix := pattern[:len(pattern)-2] // Remove "/*" - if prefix == "" { - prefix = "/" - } - return strings.HasPrefix(path, prefix) - } - - return path == pattern -} - -// CheckLocalNetwork checks if the URL is accessing local network resources -func checkLocalNetwork(parsedURL *url.URL) error { - host := parsedURL.Hostname() - - // Check for localhost variants - if host == "localhost" || host == "127.0.0.1" || host == "::1" { - return fmt.Errorf("requests to localhost are not allowed") - } - - // Try to parse as IP address - ip := net.ParseIP(host) - if ip != nil && isPrivateIP(ip) { - return fmt.Errorf("requests to private IP addresses are not allowed") - } - - return nil -} - -// IsPrivateIP checks if an IP is loopback, private, or link-local (IPv4/IPv6). -func isPrivateIP(ip net.IP) bool { - if ip == nil { - return false - } - if ip.IsLoopback() || ip.IsPrivate() { - return true - } - // IPv4 link-local: 169.254.0.0/16 - if ip4 := ip.To4(); ip4 != nil { - return ip4[0] == 169 && ip4[1] == 254 - } - // IPv6 link-local: fe80::/10 - if ip16 := ip.To16(); ip16 != nil && ip.To4() == nil { - return ip16[0] == 0xfe && (ip16[1]&0xc0) == 0x80 - } - return false -} diff --git a/plugins/host_network_permissions_base_test.go b/plugins/host_network_permissions_base_test.go deleted file mode 100644 index 9147e99ac..000000000 --- a/plugins/host_network_permissions_base_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package plugins - -import ( - "net" - "net/url" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("networkPermissionsBase", func() { - Describe("urlMatcher", func() { - var matcher *urlMatcher - - BeforeEach(func() { - matcher = newURLMatcher() - }) - - Describe("MatchesURLPattern", func() { - DescribeTable("exact URL matching", - func(requestURL, pattern string, expected bool) { - result := matcher.MatchesURLPattern(requestURL, pattern) - Expect(result).To(Equal(expected)) - }, - Entry("exact match", "https://api.example.com", "https://api.example.com", true), - Entry("different domain", "https://api.example.com", "https://api.other.com", false), - Entry("different scheme", "http://api.example.com", "https://api.example.com", false), - Entry("different path", "https://api.example.com/v1", "https://api.example.com/v2", false), - ) - - DescribeTable("wildcard pattern matching", - func(requestURL, pattern string, expected bool) { - result := matcher.MatchesURLPattern(requestURL, pattern) - Expect(result).To(Equal(expected)) - }, - Entry("universal wildcard", "https://api.example.com", "*", true), - Entry("subdomain wildcard match", "https://api.example.com", "https://*.example.com", true), - Entry("subdomain wildcard non-match", "https://api.other.com", "https://*.example.com", false), - Entry("path wildcard match", "https://api.example.com/v1/users", "https://api.example.com/*", true), - Entry("path wildcard non-match", "https://other.example.com/v1", "https://api.example.com/*", false), - Entry("port wildcard match", "https://api.example.com:8080", "https://api.example.com:*", true), - ) - }) - }) - - Describe("isPrivateIP", func() { - DescribeTable("IPv4 private IP detection", - func(ip string, expected bool) { - parsedIP := net.ParseIP(ip) - Expect(parsedIP).ToNot(BeNil(), "Failed to parse IP: %s", ip) - result := isPrivateIP(parsedIP) - Expect(result).To(Equal(expected)) - }, - // Private IPv4 ranges - Entry("10.0.0.1 (10.0.0.0/8)", "10.0.0.1", true), - Entry("10.255.255.255 (10.0.0.0/8)", "10.255.255.255", true), - Entry("172.16.0.1 (172.16.0.0/12)", "172.16.0.1", true), - Entry("172.31.255.255 (172.16.0.0/12)", "172.31.255.255", true), - Entry("192.168.1.1 (192.168.0.0/16)", "192.168.1.1", true), - Entry("192.168.255.255 (192.168.0.0/16)", "192.168.255.255", true), - Entry("127.0.0.1 (localhost)", "127.0.0.1", true), - Entry("127.255.255.255 (localhost)", "127.255.255.255", true), - Entry("169.254.1.1 (link-local)", "169.254.1.1", true), - Entry("169.254.255.255 (link-local)", "169.254.255.255", true), - - // Public IPv4 addresses - Entry("8.8.8.8 (Google DNS)", "8.8.8.8", false), - Entry("1.1.1.1 (Cloudflare DNS)", "1.1.1.1", false), - Entry("208.67.222.222 (OpenDNS)", "208.67.222.222", false), - Entry("172.15.255.255 (just outside 172.16.0.0/12)", "172.15.255.255", false), - Entry("172.32.0.1 (just outside 172.16.0.0/12)", "172.32.0.1", false), - ) - - DescribeTable("IPv6 private IP detection", - func(ip string, expected bool) { - parsedIP := net.ParseIP(ip) - Expect(parsedIP).ToNot(BeNil(), "Failed to parse IP: %s", ip) - result := isPrivateIP(parsedIP) - Expect(result).To(Equal(expected)) - }, - // Private IPv6 ranges - Entry("::1 (IPv6 localhost)", "::1", true), - Entry("fe80::1 (link-local)", "fe80::1", true), - Entry("fc00::1 (unique local)", "fc00::1", true), - Entry("fd00::1 (unique local)", "fd00::1", true), - - // Public IPv6 addresses - Entry("2001:4860:4860::8888 (Google DNS)", "2001:4860:4860::8888", false), - Entry("2606:4700:4700::1111 (Cloudflare DNS)", "2606:4700:4700::1111", false), - ) - }) - - Describe("checkLocalNetwork", func() { - DescribeTable("local network detection", - func(urlStr string, shouldError bool, expectedErrorSubstring string) { - parsedURL, err := url.Parse(urlStr) - Expect(err).ToNot(HaveOccurred()) - - err = checkLocalNetwork(parsedURL) - if shouldError { - Expect(err).To(HaveOccurred()) - if expectedErrorSubstring != "" { - Expect(err.Error()).To(ContainSubstring(expectedErrorSubstring)) - } - } else { - Expect(err).ToNot(HaveOccurred()) - } - }, - Entry("localhost", "http://localhost:8080", true, "localhost"), - Entry("127.0.0.1", "http://127.0.0.1:3000", true, "localhost"), - Entry("::1", "http://[::1]:8080", true, "localhost"), - Entry("private IP 192.168.1.100", "http://192.168.1.100", true, "private IP"), - Entry("private IP 10.0.0.1", "http://10.0.0.1", true, "private IP"), - Entry("private IP 172.16.0.1", "http://172.16.0.1", true, "private IP"), - Entry("public IP 8.8.8.8", "http://8.8.8.8", false, ""), - Entry("public domain", "https://api.example.com", false, ""), - ) - }) -}) diff --git a/plugins/host_scheduler.go b/plugins/host_scheduler.go deleted file mode 100644 index 26c5e92f8..000000000 --- a/plugins/host_scheduler.go +++ /dev/null @@ -1,338 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "sync" - "time" - - gonanoid "github.com/matoous/go-nanoid/v2" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/host/scheduler" - navidsched "github.com/navidrome/navidrome/scheduler" -) - -const ( - ScheduleTypeOneTime = "one-time" - ScheduleTypeRecurring = "recurring" -) - -// ScheduledCallback represents a registered schedule callback -type ScheduledCallback struct { - ID string - PluginID string - Type string // "one-time" or "recurring" - Payload []byte - EntryID int // Used for recurring schedules via the scheduler - Cancel context.CancelFunc // Used for one-time schedules -} - -// SchedulerHostFunctions implements the scheduler.SchedulerService interface -type SchedulerHostFunctions struct { - ss *schedulerService - pluginID string -} - -func (s SchedulerHostFunctions) ScheduleOneTime(ctx context.Context, req *scheduler.ScheduleOneTimeRequest) (*scheduler.ScheduleResponse, error) { - return s.ss.scheduleOneTime(ctx, s.pluginID, req) -} - -func (s SchedulerHostFunctions) ScheduleRecurring(ctx context.Context, req *scheduler.ScheduleRecurringRequest) (*scheduler.ScheduleResponse, error) { - return s.ss.scheduleRecurring(ctx, s.pluginID, req) -} - -func (s SchedulerHostFunctions) CancelSchedule(ctx context.Context, req *scheduler.CancelRequest) (*scheduler.CancelResponse, error) { - return s.ss.cancelSchedule(ctx, s.pluginID, req) -} - -func (s SchedulerHostFunctions) TimeNow(ctx context.Context, req *scheduler.TimeNowRequest) (*scheduler.TimeNowResponse, error) { - return s.ss.timeNow(ctx, req) -} - -type schedulerService struct { - // Map of schedule IDs to their callback info - schedules map[string]*ScheduledCallback - manager *managerImpl - navidSched navidsched.Scheduler // Navidrome scheduler for recurring jobs - mu sync.Mutex -} - -// newSchedulerService creates a new schedulerService instance -func newSchedulerService(manager *managerImpl) *schedulerService { - return &schedulerService{ - schedules: make(map[string]*ScheduledCallback), - manager: manager, - navidSched: navidsched.GetInstance(), - } -} - -func (s *schedulerService) HostFunctions(pluginID string) SchedulerHostFunctions { - return SchedulerHostFunctions{ - ss: s, - pluginID: pluginID, - } -} - -// Safe accessor methods for tests - -// hasSchedule safely checks if a schedule exists -func (s *schedulerService) hasSchedule(id string) bool { - s.mu.Lock() - defer s.mu.Unlock() - _, exists := s.schedules[id] - return exists -} - -// scheduleCount safely returns the number of schedules -func (s *schedulerService) scheduleCount() int { - s.mu.Lock() - defer s.mu.Unlock() - return len(s.schedules) -} - -// getScheduleType safely returns the type of a schedule -func (s *schedulerService) getScheduleType(id string) string { - s.mu.Lock() - defer s.mu.Unlock() - if cb, exists := s.schedules[id]; exists { - return cb.Type - } - return "" -} - -// scheduleJob is a helper function that handles the common logic for scheduling jobs -func (s *schedulerService) scheduleJob(pluginID string, scheduleId string, jobType string, payload []byte) (string, *ScheduledCallback, context.CancelFunc, error) { - if s.manager == nil { - return "", nil, nil, fmt.Errorf("scheduler service not properly initialized") - } - - // Original scheduleId (what the plugin will see) - originalScheduleId := scheduleId - if originalScheduleId == "" { - // Generate a random ID if one wasn't provided - originalScheduleId, _ = gonanoid.New(10) - } - - // Internal scheduleId (prefixed with plugin name to avoid conflicts) - internalScheduleId := pluginID + ":" + originalScheduleId - - // Store any existing cancellation function to call after we've updated the map - var cancelExisting context.CancelFunc - - // Check if there's an existing schedule with the same ID, we'll cancel it after updating the map - if existingSchedule, ok := s.schedules[internalScheduleId]; ok { - log.Debug("Replacing existing schedule with same ID", "plugin", pluginID, "scheduleID", originalScheduleId) - - // Store cancel information but don't call it yet - if existingSchedule.Type == ScheduleTypeOneTime && existingSchedule.Cancel != nil { - // We'll set the Cancel to nil to prevent the old job from removing the new one - cancelExisting = existingSchedule.Cancel - existingSchedule.Cancel = nil - } else if existingSchedule.Type == ScheduleTypeRecurring { - existingRecurringEntryID := existingSchedule.EntryID - if existingRecurringEntryID != 0 { - s.navidSched.Remove(existingRecurringEntryID) - } - } - } - - // Create the callback object - callback := &ScheduledCallback{ - ID: originalScheduleId, - PluginID: pluginID, - Type: jobType, - Payload: payload, - } - - return internalScheduleId, callback, cancelExisting, nil -} - -// scheduleOneTime registers a new one-time scheduled job -func (s *schedulerService) scheduleOneTime(_ context.Context, pluginID string, req *scheduler.ScheduleOneTimeRequest) (*scheduler.ScheduleResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - internalScheduleId, callback, cancelExisting, err := s.scheduleJob(pluginID, req.ScheduleId, ScheduleTypeOneTime, req.Payload) - if err != nil { - return nil, err - } - - // Create a context with cancel for this one-time schedule - scheduleCtx, cancel := context.WithCancel(context.Background()) - callback.Cancel = cancel - - // Store the callback info - s.schedules[internalScheduleId] = callback - - // Now that the new job is in the map, we can safely cancel the old one - if cancelExisting != nil { - // Cancel in a goroutine to avoid deadlock since we're already holding the lock - go cancelExisting() - } - - log.Debug("One-time schedule registered", "plugin", pluginID, "scheduleID", callback.ID, "internalID", internalScheduleId) - - // Start the timer goroutine with the internal ID - go s.runOneTimeSchedule(scheduleCtx, internalScheduleId, time.Duration(req.DelaySeconds)*time.Second) - - // Return the original ID to the plugin - return &scheduler.ScheduleResponse{ - ScheduleId: callback.ID, - }, nil -} - -// scheduleRecurring registers a new recurring scheduled job -func (s *schedulerService) scheduleRecurring(_ context.Context, pluginID string, req *scheduler.ScheduleRecurringRequest) (*scheduler.ScheduleResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - internalScheduleId, callback, cancelExisting, err := s.scheduleJob(pluginID, req.ScheduleId, ScheduleTypeRecurring, req.Payload) - if err != nil { - return nil, err - } - - // Schedule the job with the Navidrome scheduler - entryID, err := s.navidSched.Add(req.CronExpression, func() { - s.executeCallback(context.Background(), internalScheduleId, true) - }) - if err != nil { - return nil, fmt.Errorf("failed to schedule recurring job: %w", err) - } - - // Store the entry ID so we can cancel it later - callback.EntryID = entryID - - // Store the callback info - s.schedules[internalScheduleId] = callback - - // Now that the new job is in the map, we can safely cancel the old one - if cancelExisting != nil { - // Cancel in a goroutine to avoid deadlock since we're already holding the lock - go cancelExisting() - } - - log.Debug("Recurring schedule registered", "plugin", pluginID, "scheduleID", callback.ID, "internalID", internalScheduleId, "cron", req.CronExpression) - - // Return the original ID to the plugin - return &scheduler.ScheduleResponse{ - ScheduleId: callback.ID, - }, nil -} - -// cancelSchedule cancels a scheduled job (either one-time or recurring) -func (s *schedulerService) cancelSchedule(_ context.Context, pluginID string, req *scheduler.CancelRequest) (*scheduler.CancelResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - internalScheduleId := pluginID + ":" + req.ScheduleId - callback, exists := s.schedules[internalScheduleId] - if !exists { - return &scheduler.CancelResponse{ - Success: false, - Error: "schedule not found", - }, nil - } - - // Store the cancel functions to call after we've updated the schedule map - var cancelFunc context.CancelFunc - var recurringEntryID int - - // Store cancel information but don't call it yet - if callback.Type == ScheduleTypeOneTime && callback.Cancel != nil { - cancelFunc = callback.Cancel - callback.Cancel = nil // Set to nil to prevent the cancel handler from removing the job - } else if callback.Type == ScheduleTypeRecurring { - recurringEntryID = callback.EntryID - } - - // First remove from the map - delete(s.schedules, internalScheduleId) - - // Now perform the cancellation safely - if cancelFunc != nil { - // Execute in a goroutine to avoid deadlock since we're already holding the lock - go cancelFunc() - } - if recurringEntryID != 0 { - s.navidSched.Remove(recurringEntryID) - } - - log.Debug("Schedule canceled", "plugin", pluginID, "scheduleID", req.ScheduleId, "internalID", internalScheduleId, "type", callback.Type) - - return &scheduler.CancelResponse{ - Success: true, - }, nil -} - -// timeNow returns the current time in multiple formats -func (s *schedulerService) timeNow(_ context.Context, req *scheduler.TimeNowRequest) (*scheduler.TimeNowResponse, error) { - now := time.Now() - - return &scheduler.TimeNowResponse{ - Rfc3339Nano: now.Format(time.RFC3339Nano), - UnixMilli: now.UnixMilli(), - LocalTimeZone: now.Location().String(), - }, nil -} - -// runOneTimeSchedule handles the one-time schedule execution and callback -func (s *schedulerService) runOneTimeSchedule(ctx context.Context, internalScheduleId string, delay time.Duration) { - tmr := time.NewTimer(delay) - defer tmr.Stop() - - select { - case <-ctx.Done(): - // Schedule was cancelled via its context - // We're no longer removing the schedule here because that's handled by the code that - // cancelled the context - log.Debug("One-time schedule context canceled", "internalID", internalScheduleId) - return - - case <-tmr.C: - // Timer fired, execute the callback - s.executeCallback(ctx, internalScheduleId, false) - } -} - -// executeCallback calls the plugin's OnSchedulerCallback method -func (s *schedulerService) executeCallback(ctx context.Context, internalScheduleId string, isRecurring bool) { - s.mu.Lock() - callback := s.schedules[internalScheduleId] - // Only remove one-time schedules from the map after execution - if callback != nil && callback.Type == ScheduleTypeOneTime { - delete(s.schedules, internalScheduleId) - } - s.mu.Unlock() - - if callback == nil { - log.Error("Schedule not found for callback", "internalID", internalScheduleId) - return - } - - ctx = log.NewContext(ctx, "plugin", callback.PluginID, "scheduleID", callback.ID, "type", callback.Type) - log.Debug("Executing schedule callback") - start := time.Now() - - // Get the plugin - p := s.manager.LoadPlugin(callback.PluginID, CapabilitySchedulerCallback) - if p == nil { - log.Error("Plugin not found for callback", "plugin", callback.PluginID) - return - } - - // Type-check the plugin - plugin, ok := p.(*wasmSchedulerCallback) - if !ok { - log.Error("Plugin does not implement SchedulerCallback", "plugin", callback.PluginID) - return - } - - // Call the plugin's OnSchedulerCallback method - log.Trace(ctx, "Executing schedule callback") - err := plugin.OnSchedulerCallback(ctx, callback.ID, callback.Payload, isRecurring) - if err != nil { - log.Error("Error executing schedule callback", "elapsed", time.Since(start), err) - return - } - log.Debug("Schedule callback executed", "elapsed", time.Since(start)) -} diff --git a/plugins/host_scheduler_test.go b/plugins/host_scheduler_test.go deleted file mode 100644 index 1a3efaae9..000000000 --- a/plugins/host_scheduler_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package plugins - -import ( - "context" - "time" - - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/host/scheduler" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("SchedulerService", func() { - var ( - ss *schedulerService - manager *managerImpl - pluginName = "test_plugin" - ) - - BeforeEach(func() { - manager = createManager(nil, metrics.NewNoopInstance()) - ss = manager.schedulerService - }) - - Describe("One-time scheduling", func() { - It("schedules one-time jobs successfully", func() { - req := &scheduler.ScheduleOneTimeRequest{ - DelaySeconds: 1, - Payload: []byte("test payload"), - ScheduleId: "test-job", - } - - resp, err := ss.scheduleOneTime(context.Background(), pluginName, req) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.ScheduleId).To(Equal("test-job")) - Expect(ss.hasSchedule(pluginName + ":" + "test-job")).To(BeTrue()) - Expect(ss.getScheduleType(pluginName + ":" + "test-job")).To(Equal(ScheduleTypeOneTime)) - - // Test auto-generated ID - req.ScheduleId = "" - resp, err = ss.scheduleOneTime(context.Background(), pluginName, req) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.ScheduleId).ToNot(BeEmpty()) - }) - - It("cancels one-time jobs successfully", func() { - req := &scheduler.ScheduleOneTimeRequest{ - DelaySeconds: 10, - ScheduleId: "test-job", - } - - _, err := ss.scheduleOneTime(context.Background(), pluginName, req) - Expect(err).ToNot(HaveOccurred()) - - cancelReq := &scheduler.CancelRequest{ - ScheduleId: "test-job", - } - - resp, err := ss.cancelSchedule(context.Background(), pluginName, cancelReq) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Success).To(BeTrue()) - Expect(ss.hasSchedule(pluginName + ":" + "test-job")).To(BeFalse()) - }) - }) - - Describe("Recurring scheduling", func() { - It("schedules recurring jobs successfully", func() { - req := &scheduler.ScheduleRecurringRequest{ - CronExpression: "* * * * *", // Every minute - Payload: []byte("test payload"), - ScheduleId: "test-cron", - } - - resp, err := ss.scheduleRecurring(context.Background(), pluginName, req) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.ScheduleId).To(Equal("test-cron")) - Expect(ss.hasSchedule(pluginName + ":" + "test-cron")).To(BeTrue()) - Expect(ss.getScheduleType(pluginName + ":" + "test-cron")).To(Equal(ScheduleTypeRecurring)) - - // Test auto-generated ID - req.ScheduleId = "" - resp, err = ss.scheduleRecurring(context.Background(), pluginName, req) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.ScheduleId).ToNot(BeEmpty()) - }) - - It("cancels recurring jobs successfully", func() { - req := &scheduler.ScheduleRecurringRequest{ - CronExpression: "* * * * *", // Every minute - ScheduleId: "test-cron", - } - - _, err := ss.scheduleRecurring(context.Background(), pluginName, req) - Expect(err).ToNot(HaveOccurred()) - - cancelReq := &scheduler.CancelRequest{ - ScheduleId: "test-cron", - } - - resp, err := ss.cancelSchedule(context.Background(), pluginName, cancelReq) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Success).To(BeTrue()) - Expect(ss.hasSchedule(pluginName + ":" + "test-cron")).To(BeFalse()) - }) - }) - - Describe("Replace existing schedules", func() { - It("replaces one-time jobs with new ones", func() { - // Create first job - req1 := &scheduler.ScheduleOneTimeRequest{ - DelaySeconds: 10, - Payload: []byte("test payload 1"), - ScheduleId: "replace-job", - } - _, err := ss.scheduleOneTime(context.Background(), pluginName, req1) - Expect(err).ToNot(HaveOccurred()) - - // Verify that the initial job exists - scheduleId := pluginName + ":" + "replace-job" - Expect(ss.hasSchedule(scheduleId)).To(BeTrue(), "Initial schedule should exist") - - beforeCount := ss.scheduleCount() - - // Replace with second job using same ID - req2 := &scheduler.ScheduleOneTimeRequest{ - DelaySeconds: 60, // Use a longer delay to ensure it doesn't execute during the test - Payload: []byte("test payload 2"), - ScheduleId: "replace-job", - } - - _, err = ss.scheduleOneTime(context.Background(), pluginName, req2) - Expect(err).ToNot(HaveOccurred()) - - Eventually(func() bool { - return ss.hasSchedule(scheduleId) - }).Should(BeTrue(), "Schedule should exist after replacement") - Expect(ss.scheduleCount()).To(Equal(beforeCount), "Job count should remain the same after replacement") - }) - - It("replaces recurring jobs with new ones", func() { - // Create first job - req1 := &scheduler.ScheduleRecurringRequest{ - CronExpression: "0 * * * *", - Payload: []byte("test payload 1"), - ScheduleId: "replace-cron", - } - _, err := ss.scheduleRecurring(context.Background(), pluginName, req1) - Expect(err).ToNot(HaveOccurred()) - - beforeCount := ss.scheduleCount() - - // Replace with second job using same ID - req2 := &scheduler.ScheduleRecurringRequest{ - CronExpression: "*/5 * * * *", - Payload: []byte("test payload 2"), - ScheduleId: "replace-cron", - } - - _, err = ss.scheduleRecurring(context.Background(), pluginName, req2) - Expect(err).ToNot(HaveOccurred()) - - Eventually(func() bool { - return ss.hasSchedule(pluginName + ":" + "replace-cron") - }).Should(BeTrue(), "Schedule should exist after replacement") - Expect(ss.scheduleCount()).To(Equal(beforeCount), "Job count should remain the same after replacement") - }) - }) - - Describe("TimeNow", func() { - It("returns current time in RFC3339Nano, Unix milliseconds, and local timezone", func() { - now := time.Now() - req := &scheduler.TimeNowRequest{} - resp, err := ss.timeNow(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(resp.UnixMilli).To(BeNumerically(">=", now.UnixMilli())) - Expect(resp.LocalTimeZone).ToNot(BeEmpty()) - - // Validate RFC3339Nano format can be parsed - parsedTime, parseErr := time.Parse(time.RFC3339Nano, resp.Rfc3339Nano) - Expect(parseErr).ToNot(HaveOccurred()) - - // Validate that Unix milliseconds is reasonably close to the RFC3339Nano time - expectedMillis := parsedTime.UnixMilli() - Expect(resp.UnixMilli).To(Equal(expectedMillis)) - - // Validate local timezone matches the current system timezone - expectedTimezone := now.Location().String() - Expect(resp.LocalTimeZone).To(Equal(expectedTimezone)) - }) - }) -}) diff --git a/plugins/host_subsonicapi.go b/plugins/host_subsonicapi.go deleted file mode 100644 index 937dd044f..000000000 --- a/plugins/host_subsonicapi.go +++ /dev/null @@ -1,170 +0,0 @@ -package plugins - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "path" - "strings" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/model/request" - "github.com/navidrome/navidrome/plugins/host/subsonicapi" - "github.com/navidrome/navidrome/plugins/schema" - "github.com/navidrome/navidrome/server/subsonic" -) - -// SubsonicAPIService is the interface for the Subsonic API service -// -// Authentication: The plugin must provide valid authentication parameters in the URL: -// - Required: `u` (username) - The service validates this parameter is present -// - Example: `"/rest/ping?u=admin"` -// -// URL Format: Only the path and query parameters from the URL are used - host, protocol, and method are ignored -// -// Automatic Parameters: The service automatically adds: -// - `c`: Plugin name (client identifier) -// - `v`: Subsonic API version (1.16.1) -// - `f`: Response format (json) -// -// See example usage in the `plugins/examples/subsonicapi-demo` plugin -type subsonicAPIServiceImpl struct { - pluginID string - router SubsonicRouter - ds model.DataStore - permissions *subsonicAPIPermissions -} - -func newSubsonicAPIService(pluginID string, router *SubsonicRouter, ds model.DataStore, permissions *schema.PluginManifestPermissionsSubsonicapi) subsonicapi.SubsonicAPIService { - return &subsonicAPIServiceImpl{ - pluginID: pluginID, - router: *router, - ds: ds, - permissions: parseSubsonicAPIPermissions(permissions), - } -} - -func (s *subsonicAPIServiceImpl) Call(ctx context.Context, req *subsonicapi.CallRequest) (*subsonicapi.CallResponse, error) { - if s.router == nil { - return &subsonicapi.CallResponse{ - Error: "SubsonicAPI router not available", - }, nil - } - - // Parse the input URL - parsedURL, err := url.Parse(req.Url) - if err != nil { - return &subsonicapi.CallResponse{ - Error: fmt.Sprintf("invalid URL format: %v", err), - }, nil - } - - // Extract query parameters - query := parsedURL.Query() - - // Validate that 'u' (username) parameter is present - username := query.Get("u") - if username == "" { - return &subsonicapi.CallResponse{ - Error: "missing required parameter 'u' (username)", - }, nil - } - - if err := s.checkPermissions(ctx, username); err != nil { - log.Warn(ctx, "SubsonicAPI call blocked by permissions", "plugin", s.pluginID, "user", username, err) - return &subsonicapi.CallResponse{Error: err.Error()}, nil - } - - // Add required Subsonic API parameters - query.Set("c", s.pluginID) // Client name (plugin ID) - query.Set("f", "json") // Response format - query.Set("v", subsonic.Version) // API version - - // Extract the endpoint from the path - endpoint := path.Base(parsedURL.Path) - - // Build the final URL with processed path and modified query parameters - finalURL := &url.URL{ - Path: "/" + endpoint, - RawQuery: query.Encode(), - } - - // Create HTTP request with a fresh context to avoid Chi RouteContext pollution. - // Using http.NewRequest (instead of http.NewRequestWithContext) ensures the internal - // SubsonicAPI call doesn't inherit routing information from the parent handler, - // which would cause Chi to invoke the wrong handler. Authentication context is - // explicitly added in the next step via request.WithInternalAuth. - httpReq, err := http.NewRequest("GET", finalURL.String(), nil) - if err != nil { - return &subsonicapi.CallResponse{ - Error: fmt.Sprintf("failed to create HTTP request: %v", err), - }, nil - } - - // Set internal authentication context using the username from the 'u' parameter - authCtx := request.WithInternalAuth(httpReq.Context(), username) - httpReq = httpReq.WithContext(authCtx) - - // Use ResponseRecorder to capture the response - recorder := httptest.NewRecorder() - - // Call the subsonic router - s.router.ServeHTTP(recorder, httpReq) - - // Return the response body as JSON - return &subsonicapi.CallResponse{ - Json: recorder.Body.String(), - }, nil -} - -func (s *subsonicAPIServiceImpl) checkPermissions(ctx context.Context, username string) error { - if s.permissions == nil { - return nil - } - if len(s.permissions.AllowedUsernames) > 0 { - if _, ok := s.permissions.usernameMap[strings.ToLower(username)]; !ok { - return fmt.Errorf("username %s is not allowed", username) - } - } - if !s.permissions.AllowAdmins { - if s.router == nil { - return fmt.Errorf("permissions check failed: router not available") - } - usr, err := s.ds.User(ctx).FindByUsername(username) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - return fmt.Errorf("username %s not found", username) - } - return err - } - if usr.IsAdmin { - return fmt.Errorf("calling SubsonicAPI as admin user is not allowed") - } - } - return nil -} - -type subsonicAPIPermissions struct { - AllowedUsernames []string - AllowAdmins bool - usernameMap map[string]struct{} -} - -func parseSubsonicAPIPermissions(data *schema.PluginManifestPermissionsSubsonicapi) *subsonicAPIPermissions { - if data == nil { - return &subsonicAPIPermissions{} - } - perms := &subsonicAPIPermissions{ - AllowedUsernames: data.AllowedUsernames, - AllowAdmins: data.AllowAdmins, - usernameMap: make(map[string]struct{}), - } - for _, u := range data.AllowedUsernames { - perms.usernameMap[strings.ToLower(u)] = struct{}{} - } - return perms -} diff --git a/plugins/host_subsonicapi_test.go b/plugins/host_subsonicapi_test.go deleted file mode 100644 index a3161ff06..000000000 --- a/plugins/host_subsonicapi_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package plugins - -import ( - "context" - "net/http" - - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/model/request" - "github.com/navidrome/navidrome/plugins/host/subsonicapi" - "github.com/navidrome/navidrome/plugins/schema" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("SubsonicAPI Host Service", func() { - var ( - service *subsonicAPIServiceImpl - mockRouter http.Handler - userRepo *tests.MockedUserRepo - ) - - BeforeEach(func() { - // Setup mock datastore with users - userRepo = tests.CreateMockUserRepo() - _ = userRepo.Put(&model.User{UserName: "admin", IsAdmin: true}) - _ = userRepo.Put(&model.User{UserName: "user", IsAdmin: false}) - ds := &tests.MockDataStore{MockedUser: userRepo} - - // Create a mock router - mockRouter = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"subsonic-response":{"status":"ok","version":"1.16.1"}}`)) - }) - - // Create service implementation - service = &subsonicAPIServiceImpl{ - pluginID: "test-plugin", - router: mockRouter, - ds: ds, - } - }) - - // Helper function to create a mock router that captures the request - setupRequestCapture := func() **http.Request { - var capturedRequest *http.Request - mockRouter = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - capturedRequest = r - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) - }) - service.router = mockRouter - return &capturedRequest - } - - Describe("Call", func() { - Context("when subsonic router is available", func() { - It("should process the request successfully", func() { - req := &subsonicapi.CallRequest{ - Url: "/rest/ping?u=admin", - } - - resp, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Error).To(BeEmpty()) - Expect(resp.Json).To(ContainSubstring("subsonic-response")) - Expect(resp.Json).To(ContainSubstring("ok")) - }) - - It("should add required parameters to the URL", func() { - capturedRequestPtr := setupRequestCapture() - - req := &subsonicapi.CallRequest{ - Url: "/rest/getAlbum.view?id=123&u=admin", - } - - _, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(*capturedRequestPtr).ToNot(BeNil()) - - query := (*capturedRequestPtr).URL.Query() - Expect(query.Get("c")).To(Equal("test-plugin")) - Expect(query.Get("f")).To(Equal("json")) - Expect(query.Get("v")).To(Equal("1.16.1")) - Expect(query.Get("id")).To(Equal("123")) - Expect(query.Get("u")).To(Equal("admin")) - }) - - It("should only use path and query from the input URL", func() { - capturedRequestPtr := setupRequestCapture() - - req := &subsonicapi.CallRequest{ - Url: "https://external.example.com:8080/rest/ping?u=admin", - } - - _, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(*capturedRequestPtr).ToNot(BeNil()) - Expect((*capturedRequestPtr).URL.Path).To(Equal("/ping")) - Expect((*capturedRequestPtr).URL.Host).To(BeEmpty()) - Expect((*capturedRequestPtr).URL.Scheme).To(BeEmpty()) - }) - - It("ignores the path prefix in the URL", func() { - capturedRequestPtr := setupRequestCapture() - - req := &subsonicapi.CallRequest{ - Url: "/basepath/rest/ping?u=admin", - } - - _, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(*capturedRequestPtr).ToNot(BeNil()) - Expect((*capturedRequestPtr).URL.Path).To(Equal("/ping")) - }) - - It("should set internal authentication with username from 'u' parameter", func() { - capturedRequestPtr := setupRequestCapture() - - req := &subsonicapi.CallRequest{ - Url: "/rest/ping?u=testuser", - } - - _, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(*capturedRequestPtr).ToNot(BeNil()) - - // Verify that internal authentication is set in the context - username, ok := request.InternalAuthFrom((*capturedRequestPtr).Context()) - Expect(ok).To(BeTrue()) - Expect(username).To(Equal("testuser")) - }) - }) - - Context("when subsonic router is not available", func() { - BeforeEach(func() { - service.router = nil - }) - - It("should return an error", func() { - req := &subsonicapi.CallRequest{ - Url: "/rest/ping?u=admin", - } - - resp, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Error).To(Equal("SubsonicAPI router not available")) - Expect(resp.Json).To(BeEmpty()) - }) - }) - - Context("when URL is invalid", func() { - It("should return an error for malformed URLs", func() { - req := &subsonicapi.CallRequest{ - Url: "://invalid-url", - } - - resp, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Error).To(ContainSubstring("invalid URL format")) - Expect(resp.Json).To(BeEmpty()) - }) - - It("should return an error when 'u' parameter is missing", func() { - req := &subsonicapi.CallRequest{ - Url: "/rest/ping?p=password", - } - - resp, err := service.Call(context.Background(), req) - - Expect(err).ToNot(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Error).To(Equal("missing required parameter 'u' (username)")) - Expect(resp.Json).To(BeEmpty()) - }) - }) - - Context("permission checks", func() { - It("rejects disallowed username", func() { - service.permissions = parseSubsonicAPIPermissions(&schema.PluginManifestPermissionsSubsonicapi{ - Reason: "test", - AllowedUsernames: []string{"user"}, - }) - - resp, err := service.Call(context.Background(), &subsonicapi.CallRequest{Url: "/rest/ping?u=admin"}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Error).To(ContainSubstring("not allowed")) - }) - - It("rejects admin when allowAdmins is false", func() { - service.permissions = parseSubsonicAPIPermissions(&schema.PluginManifestPermissionsSubsonicapi{Reason: "test"}) - - resp, err := service.Call(context.Background(), &subsonicapi.CallRequest{Url: "/rest/ping?u=admin"}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Error).To(ContainSubstring("not allowed")) - }) - - It("allows admin when allowAdmins is true", func() { - service.permissions = parseSubsonicAPIPermissions(&schema.PluginManifestPermissionsSubsonicapi{Reason: "test", AllowAdmins: true}) - - resp, err := service.Call(context.Background(), &subsonicapi.CallRequest{Url: "/rest/ping?u=admin"}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Error).To(BeEmpty()) - }) - }) - }) -}) diff --git a/plugins/host_websocket.go b/plugins/host_websocket.go deleted file mode 100644 index e90d1363d..000000000 --- a/plugins/host_websocket.go +++ /dev/null @@ -1,400 +0,0 @@ -package plugins - -import ( - "context" - "encoding/binary" - "fmt" - "strings" - "sync" - "time" - - gorillaws "github.com/gorilla/websocket" - gonanoid "github.com/matoous/go-nanoid/v2" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/websocket" -) - -// WebSocketConnection represents a WebSocket connection -type WebSocketConnection struct { - Conn *gorillaws.Conn - PluginName string - ConnectionID string - Done chan struct{} - mu sync.Mutex -} - -// WebSocketHostFunctions implements the websocket.WebSocketService interface -type WebSocketHostFunctions struct { - ws *websocketService - pluginID string - permissions *webSocketPermissions -} - -func (s WebSocketHostFunctions) Connect(ctx context.Context, req *websocket.ConnectRequest) (*websocket.ConnectResponse, error) { - return s.ws.connect(ctx, s.pluginID, req, s.permissions) -} - -func (s WebSocketHostFunctions) SendText(ctx context.Context, req *websocket.SendTextRequest) (*websocket.SendTextResponse, error) { - return s.ws.sendText(ctx, s.pluginID, req) -} - -func (s WebSocketHostFunctions) SendBinary(ctx context.Context, req *websocket.SendBinaryRequest) (*websocket.SendBinaryResponse, error) { - return s.ws.sendBinary(ctx, s.pluginID, req) -} - -func (s WebSocketHostFunctions) Close(ctx context.Context, req *websocket.CloseRequest) (*websocket.CloseResponse, error) { - return s.ws.close(ctx, s.pluginID, req) -} - -// websocketService implements the WebSocket service functionality -type websocketService struct { - connections map[string]*WebSocketConnection - manager *managerImpl - mu sync.RWMutex -} - -// newWebsocketService creates a new websocketService instance -func newWebsocketService(manager *managerImpl) *websocketService { - return &websocketService{ - connections: make(map[string]*WebSocketConnection), - manager: manager, - } -} - -// HostFunctions returns the WebSocketHostFunctions for the given plugin -func (s *websocketService) HostFunctions(pluginID string, permissions *webSocketPermissions) WebSocketHostFunctions { - return WebSocketHostFunctions{ - ws: s, - pluginID: pluginID, - permissions: permissions, - } -} - -// Safe accessor methods - -// hasConnection safely checks if a connection exists -func (s *websocketService) hasConnection(id string) bool { - s.mu.RLock() - defer s.mu.RUnlock() - _, exists := s.connections[id] - return exists -} - -// connectionCount safely returns the number of connections -func (s *websocketService) connectionCount() int { - s.mu.RLock() - defer s.mu.RUnlock() - return len(s.connections) -} - -// getConnection safely retrieves a connection by internal ID -func (s *websocketService) getConnection(internalConnectionID string) (*WebSocketConnection, error) { - s.mu.RLock() - defer s.mu.RUnlock() - conn, exists := s.connections[internalConnectionID] - - if !exists { - return nil, fmt.Errorf("connection not found") - } - return conn, nil -} - -// internalConnectionID builds the internal connection ID from plugin and connection ID -func internalConnectionID(pluginName, connectionID string) string { - return pluginName + ":" + connectionID -} - -// extractConnectionID extracts the original connection ID from an internal ID -func extractConnectionID(internalID string) (string, error) { - parts := strings.Split(internalID, ":") - if len(parts) != 2 { - return "", fmt.Errorf("invalid internal connection ID format: %s", internalID) - } - return parts[1], nil -} - -// connect establishes a new WebSocket connection -func (s *websocketService) connect(ctx context.Context, pluginID string, req *websocket.ConnectRequest, permissions *webSocketPermissions) (*websocket.ConnectResponse, error) { - if s.manager == nil { - return nil, fmt.Errorf("websocket service not properly initialized") - } - - // Check permissions if they exist - if permissions != nil { - if err := permissions.IsConnectionAllowed(req.Url); err != nil { - log.Warn(ctx, "WebSocket connection blocked by permissions", "plugin", pluginID, "url", req.Url, err) - return &websocket.ConnectResponse{Error: "Connection blocked by plugin permissions: " + err.Error()}, nil - } - } - - // Create websocket dialer with the headers - dialer := gorillaws.DefaultDialer - header := make(map[string][]string) - for k, v := range req.Headers { - header[k] = []string{v} - } - - // Connect to the WebSocket server - conn, resp, err := dialer.DialContext(ctx, req.Url, header) - if err != nil { - return nil, fmt.Errorf("failed to connect to WebSocket server: %w", err) - } - defer resp.Body.Close() - - // Generate a connection ID - if req.ConnectionId == "" { - req.ConnectionId, _ = gonanoid.New(10) - } - connectionID := req.ConnectionId - internal := internalConnectionID(pluginID, connectionID) - - // Create the connection object - wsConn := &WebSocketConnection{ - Conn: conn, - PluginName: pluginID, - ConnectionID: connectionID, - Done: make(chan struct{}), - } - - // Store the connection - s.mu.Lock() - defer s.mu.Unlock() - s.connections[internal] = wsConn - - log.Debug("WebSocket connection established", "plugin", pluginID, "connectionID", connectionID, "url", req.Url) - - // Start the message handling goroutine - go s.handleMessages(internal, wsConn) - - return &websocket.ConnectResponse{ - ConnectionId: connectionID, - }, nil -} - -// writeMessage is a helper to send messages to a websocket connection -func (s *websocketService) writeMessage(pluginID string, connID string, messageType int, data []byte) error { - internal := internalConnectionID(pluginID, connID) - - conn, err := s.getConnection(internal) - if err != nil { - return err - } - - conn.mu.Lock() - defer conn.mu.Unlock() - - if err := conn.Conn.WriteMessage(messageType, data); err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - - return nil -} - -// sendText sends a text message over a WebSocket connection -func (s *websocketService) sendText(ctx context.Context, pluginID string, req *websocket.SendTextRequest) (*websocket.SendTextResponse, error) { - if err := s.writeMessage(pluginID, req.ConnectionId, gorillaws.TextMessage, []byte(req.Message)); err != nil { - return &websocket.SendTextResponse{Error: err.Error()}, nil //nolint:nilerr - } - return &websocket.SendTextResponse{}, nil -} - -// sendBinary sends binary data over a WebSocket connection -func (s *websocketService) sendBinary(ctx context.Context, pluginID string, req *websocket.SendBinaryRequest) (*websocket.SendBinaryResponse, error) { - if err := s.writeMessage(pluginID, req.ConnectionId, gorillaws.BinaryMessage, req.Data); err != nil { - return &websocket.SendBinaryResponse{Error: err.Error()}, nil //nolint:nilerr - } - return &websocket.SendBinaryResponse{}, nil -} - -// close closes a WebSocket connection -func (s *websocketService) close(ctx context.Context, pluginID string, req *websocket.CloseRequest) (*websocket.CloseResponse, error) { - internal := internalConnectionID(pluginID, req.ConnectionId) - - s.mu.Lock() - conn, exists := s.connections[internal] - if !exists { - s.mu.Unlock() - return &websocket.CloseResponse{Error: "connection not found"}, nil - } - delete(s.connections, internal) - s.mu.Unlock() - - // Signal the message handling goroutine to stop - close(conn.Done) - - // Close the connection with the specified code and reason - conn.mu.Lock() - defer conn.mu.Unlock() - - err := conn.Conn.WriteControl( - gorillaws.CloseMessage, - gorillaws.FormatCloseMessage(int(req.Code), req.Reason), - time.Now().Add(time.Second), - ) - if err != nil { - log.Error("Error sending close message", "plugin", pluginID, "error", err) - } - - if err := conn.Conn.Close(); err != nil { - return nil, fmt.Errorf("error closing connection: %w", err) - } - - log.Debug("WebSocket connection closed", "plugin", pluginID, "connectionID", req.ConnectionId) - return &websocket.CloseResponse{}, nil -} - -// handleMessages processes incoming WebSocket messages -func (s *websocketService) handleMessages(internalID string, conn *WebSocketConnection) { - // Get the original connection ID (without plugin prefix) - connectionID, err := extractConnectionID(internalID) - if err != nil { - log.Error("Invalid internal connection ID", "id", internalID, "error", err) - return - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - defer func() { - // Ensure the connection is removed from the map if not already removed - s.mu.Lock() - defer s.mu.Unlock() - delete(s.connections, internalID) - - log.Debug("WebSocket message handler stopped", "plugin", conn.PluginName, "connectionID", connectionID) - }() - - // Add connection info to context - ctx = log.NewContext(ctx, - "connectionID", connectionID, - "plugin", conn.PluginName, - ) - - for { - select { - case <-conn.Done: - // Connection was closed by a Close call - return - default: - // Set a read deadline - _ = conn.Conn.SetReadDeadline(time.Now().Add(time.Second * 60)) - - // Read the next message - messageType, message, err := conn.Conn.ReadMessage() - if err != nil { - s.notifyErrorCallback(ctx, connectionID, conn, err.Error()) - return - } - - // Reset the read deadline - _ = conn.Conn.SetReadDeadline(time.Time{}) - - // Process the message based on its type - switch messageType { - case gorillaws.TextMessage: - s.notifyTextCallback(ctx, connectionID, conn, string(message)) - case gorillaws.BinaryMessage: - s.notifyBinaryCallback(ctx, connectionID, conn, message) - case gorillaws.CloseMessage: - code := gorillaws.CloseNormalClosure - reason := "" - if len(message) >= 2 { - code = int(binary.BigEndian.Uint16(message[:2])) - if len(message) > 2 { - reason = string(message[2:]) - } - } - s.notifyCloseCallback(ctx, connectionID, conn, code, reason) - return - } - } - } -} - -// executeCallback is a common function that handles the plugin loading and execution -// for all types of callbacks -func (s *websocketService) executeCallback(ctx context.Context, pluginID, methodName string, fn func(context.Context, api.WebSocketCallback) error) { - log.Debug(ctx, "WebSocket received") - - start := time.Now() - - // Get the plugin - p := s.manager.LoadPlugin(pluginID, CapabilityWebSocketCallback) - if p == nil { - log.Error(ctx, "Plugin not found for WebSocket callback") - return - } - - _, _ = callMethod(ctx, p, methodName, func(inst api.WebSocketCallback) (struct{}, error) { - // Call the appropriate callback function - log.Trace(ctx, "Executing WebSocket callback") - if err := fn(ctx, inst); err != nil { - log.Error(ctx, "Error executing WebSocket callback", "elapsed", time.Since(start), err) - return struct{}{}, fmt.Errorf("error executing WebSocket callback: %w", err) - } - log.Debug(ctx, "WebSocket callback executed", "elapsed", time.Since(start)) - return struct{}{}, nil - }) -} - -// notifyTextCallback notifies the plugin of a text message -func (s *websocketService) notifyTextCallback(ctx context.Context, connectionID string, conn *WebSocketConnection, message string) { - req := &api.OnTextMessageRequest{ - ConnectionId: connectionID, - Message: message, - } - - ctx = log.NewContext(ctx, "callback", "OnTextMessage", "size", len(message)) - - s.executeCallback(ctx, conn.PluginName, "OnTextMessage", func(ctx context.Context, plugin api.WebSocketCallback) error { - _, err := checkErr(plugin.OnTextMessage(ctx, req)) - return err - }) -} - -// notifyBinaryCallback notifies the plugin of a binary message -func (s *websocketService) notifyBinaryCallback(ctx context.Context, connectionID string, conn *WebSocketConnection, data []byte) { - req := &api.OnBinaryMessageRequest{ - ConnectionId: connectionID, - Data: data, - } - - ctx = log.NewContext(ctx, "callback", "OnBinaryMessage", "size", len(data)) - - s.executeCallback(ctx, conn.PluginName, "OnBinaryMessage", func(ctx context.Context, plugin api.WebSocketCallback) error { - _, err := checkErr(plugin.OnBinaryMessage(ctx, req)) - return err - }) -} - -// notifyErrorCallback notifies the plugin of an error -func (s *websocketService) notifyErrorCallback(ctx context.Context, connectionID string, conn *WebSocketConnection, errorMsg string) { - req := &api.OnErrorRequest{ - ConnectionId: connectionID, - Error: errorMsg, - } - - ctx = log.NewContext(ctx, "callback", "OnError", "error", errorMsg) - - s.executeCallback(ctx, conn.PluginName, "OnError", func(ctx context.Context, plugin api.WebSocketCallback) error { - _, err := checkErr(plugin.OnError(ctx, req)) - return err - }) -} - -// notifyCloseCallback notifies the plugin that the connection was closed -func (s *websocketService) notifyCloseCallback(ctx context.Context, connectionID string, conn *WebSocketConnection, code int, reason string) { - req := &api.OnCloseRequest{ - ConnectionId: connectionID, - Code: int32(code), - Reason: reason, - } - - ctx = log.NewContext(ctx, "callback", "OnClose", "code", code, "reason", reason) - - s.executeCallback(ctx, conn.PluginName, "OnClose", func(ctx context.Context, plugin api.WebSocketCallback) error { - _, err := checkErr(plugin.OnClose(ctx, req)) - return err - }) -} diff --git a/plugins/host_websocket_permissions.go b/plugins/host_websocket_permissions.go deleted file mode 100644 index 53f6a127b..000000000 --- a/plugins/host_websocket_permissions.go +++ /dev/null @@ -1,76 +0,0 @@ -package plugins - -import ( - "fmt" - - "github.com/navidrome/navidrome/plugins/schema" -) - -// WebSocketPermissions represents granular WebSocket access permissions for plugins -type webSocketPermissions struct { - *networkPermissionsBase - AllowedUrls []string `json:"allowedUrls"` - matcher *urlMatcher -} - -// parseWebSocketPermissions extracts WebSocket permissions from the schema -func parseWebSocketPermissions(permData *schema.PluginManifestPermissionsWebsocket) (*webSocketPermissions, error) { - if len(permData.AllowedUrls) == 0 { - return nil, fmt.Errorf("allowedUrls must contain at least one URL pattern") - } - - return &webSocketPermissions{ - networkPermissionsBase: &networkPermissionsBase{ - AllowLocalNetwork: permData.AllowLocalNetwork, - }, - AllowedUrls: permData.AllowedUrls, - matcher: newURLMatcher(), - }, nil -} - -// IsConnectionAllowed checks if a WebSocket connection is allowed -func (w *webSocketPermissions) IsConnectionAllowed(requestURL string) error { - if _, err := checkURLPolicy(requestURL, w.AllowLocalNetwork); err != nil { - return err - } - - // allowedUrls is required - no fallback to allow all URLs - if len(w.AllowedUrls) == 0 { - return fmt.Errorf("no allowed URLs configured for plugin") - } - - // Check URL patterns - // First try exact matches, then wildcard matches - - // Phase 1: Check for exact matches first - for _, urlPattern := range w.AllowedUrls { - if urlPattern == "*" || (!containsWildcard(urlPattern) && w.matcher.MatchesURLPattern(requestURL, urlPattern)) { - return nil - } - } - - // Phase 2: Check wildcard patterns - for _, urlPattern := range w.AllowedUrls { - if containsWildcard(urlPattern) && w.matcher.MatchesURLPattern(requestURL, urlPattern) { - return nil - } - } - - return fmt.Errorf("URL %s does not match any allowed URL patterns", requestURL) -} - -// containsWildcard checks if a URL pattern contains wildcard characters -func containsWildcard(pattern string) bool { - if pattern == "*" { - return true - } - - // Check for wildcards anywhere in the pattern - for _, char := range pattern { - if char == '*' { - return true - } - } - - return false -} diff --git a/plugins/host_websocket_permissions_test.go b/plugins/host_websocket_permissions_test.go deleted file mode 100644 index e794ca6ad..000000000 --- a/plugins/host_websocket_permissions_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package plugins - -import ( - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("WebSocket Permissions", func() { - Describe("parseWebSocketPermissions", func() { - It("should parse valid WebSocket permissions", func() { - permData := &schema.PluginManifestPermissionsWebsocket{ - Reason: "Need to connect to WebSocket API", - AllowLocalNetwork: false, - AllowedUrls: []string{"wss://api.example.com/ws", "wss://cdn.example.com/*"}, - } - - perms, err := parseWebSocketPermissions(permData) - Expect(err).To(BeNil()) - Expect(perms).ToNot(BeNil()) - Expect(perms.AllowLocalNetwork).To(BeFalse()) - Expect(perms.AllowedUrls).To(Equal([]string{"wss://api.example.com/ws", "wss://cdn.example.com/*"})) - }) - - It("should fail if allowedUrls is empty", func() { - permData := &schema.PluginManifestPermissionsWebsocket{ - Reason: "Need to connect to WebSocket API", - AllowLocalNetwork: false, - AllowedUrls: []string{}, - } - - _, err := parseWebSocketPermissions(permData) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("allowedUrls must contain at least one URL pattern")) - }) - - It("should handle wildcard patterns", func() { - permData := &schema.PluginManifestPermissionsWebsocket{ - Reason: "Need to connect to any WebSocket", - AllowLocalNetwork: true, - AllowedUrls: []string{"wss://*"}, - } - - perms, err := parseWebSocketPermissions(permData) - Expect(err).To(BeNil()) - Expect(perms.AllowLocalNetwork).To(BeTrue()) - Expect(perms.AllowedUrls).To(Equal([]string{"wss://*"})) - }) - - Context("URL matching", func() { - var perms *webSocketPermissions - - BeforeEach(func() { - permData := &schema.PluginManifestPermissionsWebsocket{ - Reason: "Need to connect to external services", - AllowLocalNetwork: true, - AllowedUrls: []string{"wss://api.example.com/*", "ws://localhost:8080"}, - } - var err error - perms, err = parseWebSocketPermissions(permData) - Expect(err).To(BeNil()) - }) - - It("should allow connections to URLs matching patterns", func() { - err := perms.IsConnectionAllowed("wss://api.example.com/v1/stream") - Expect(err).To(BeNil()) - - err = perms.IsConnectionAllowed("ws://localhost:8080") - Expect(err).To(BeNil()) - }) - - It("should deny connections to URLs not matching patterns", func() { - err := perms.IsConnectionAllowed("wss://malicious.com/stream") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("does not match any allowed URL patterns")) - }) - }) - }) -}) diff --git a/plugins/host_websocket_test.go b/plugins/host_websocket_test.go deleted file mode 100644 index ecadc6463..000000000 --- a/plugins/host_websocket_test.go +++ /dev/null @@ -1,231 +0,0 @@ -package plugins - -import ( - "context" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - "time" - - gorillaws "github.com/gorilla/websocket" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/host/websocket" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("WebSocket Host Service", func() { - var ( - wsService *websocketService - manager *managerImpl - ctx context.Context - server *httptest.Server - upgrader gorillaws.Upgrader - serverMessages []string - serverMu sync.Mutex - ) - - // WebSocket echo server handler - echoHandler := func(w http.ResponseWriter, r *http.Request) { - // Check headers - if r.Header.Get("X-Test-Header") != "test-value" { - http.Error(w, "Missing or invalid X-Test-Header", http.StatusBadRequest) - return - } - - // Upgrade connection to WebSocket - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - defer conn.Close() - - // Echo messages back - for { - mt, message, err := conn.ReadMessage() - if err != nil { - break - } - - // Store the received message for verification - if mt == gorillaws.TextMessage { - msg := string(message) - serverMu.Lock() - serverMessages = append(serverMessages, msg) - serverMu.Unlock() - } - - // Echo it back - err = conn.WriteMessage(mt, message) - if err != nil { - break - } - - // If message is "close", close the connection - if mt == gorillaws.TextMessage && string(message) == "close" { - _ = conn.WriteControl( - gorillaws.CloseMessage, - gorillaws.FormatCloseMessage(gorillaws.CloseNormalClosure, "bye"), - time.Now().Add(time.Second), - ) - break - } - } - } - - BeforeEach(func() { - ctx = context.Background() - serverMessages = make([]string, 0) - serverMu = sync.Mutex{} - - // Create a test WebSocket server - //upgrader = gorillaws.Upgrader{} - server = httptest.NewServer(http.HandlerFunc(echoHandler)) - DeferCleanup(server.Close) - - // Create a new manager and websocket service - manager = createManager(nil, metrics.NewNoopInstance()) - wsService = newWebsocketService(manager) - }) - - Describe("WebSocket operations", func() { - var ( - pluginName string - connectionID string - wsURL string - ) - - BeforeEach(func() { - pluginName = "test-plugin" - connectionID = "test-connection-id" - wsURL = "ws" + strings.TrimPrefix(server.URL, "http") - }) - - It("connects to a WebSocket server", func() { - // Connect to the WebSocket server - req := &websocket.ConnectRequest{ - Url: wsURL, - Headers: map[string]string{ - "X-Test-Header": "test-value", - }, - ConnectionId: connectionID, - } - - resp, err := wsService.connect(ctx, pluginName, req, nil) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.ConnectionId).ToNot(BeEmpty()) - connectionID = resp.ConnectionId - - // Verify that the connection was added to the service - internalID := pluginName + ":" + connectionID - Expect(wsService.hasConnection(internalID)).To(BeTrue()) - }) - - It("sends and receives text messages", func() { - // Connect to the WebSocket server - req := &websocket.ConnectRequest{ - Url: wsURL, - Headers: map[string]string{ - "X-Test-Header": "test-value", - }, - ConnectionId: connectionID, - } - - resp, err := wsService.connect(ctx, pluginName, req, nil) - Expect(err).ToNot(HaveOccurred()) - connectionID = resp.ConnectionId - - // Send a text message - textReq := &websocket.SendTextRequest{ - ConnectionId: connectionID, - Message: "hello websocket", - } - - _, err = wsService.sendText(ctx, pluginName, textReq) - Expect(err).ToNot(HaveOccurred()) - - // Wait a bit for the message to be processed - Eventually(func() []string { - serverMu.Lock() - defer serverMu.Unlock() - return serverMessages - }, "1s").Should(ContainElement("hello websocket")) - }) - - It("closes a WebSocket connection", func() { - // Connect to the WebSocket server - req := &websocket.ConnectRequest{ - Url: wsURL, - Headers: map[string]string{ - "X-Test-Header": "test-value", - }, - ConnectionId: connectionID, - } - - resp, err := wsService.connect(ctx, pluginName, req, nil) - Expect(err).ToNot(HaveOccurred()) - connectionID = resp.ConnectionId - - initialCount := wsService.connectionCount() - - // Close the connection - closeReq := &websocket.CloseRequest{ - ConnectionId: connectionID, - Code: 1000, // Normal closure - Reason: "test complete", - } - - _, err = wsService.close(ctx, pluginName, closeReq) - Expect(err).ToNot(HaveOccurred()) - - // Verify that the connection was removed - Eventually(func() int { - return wsService.connectionCount() - }, "1s").Should(Equal(initialCount - 1)) - - internalID := pluginName + ":" + connectionID - Expect(wsService.hasConnection(internalID)).To(BeFalse()) - }) - - It("handles connection errors gracefully", func() { - if testing.Short() { - GinkgoT().Skip("skipping test in short mode.") - } - - // Try to connect to an invalid URL - req := &websocket.ConnectRequest{ - Url: "ws://invalid-url-that-does-not-exist", - Headers: map[string]string{}, - ConnectionId: connectionID, - } - - _, err := wsService.connect(ctx, pluginName, req, nil) - Expect(err).To(HaveOccurred()) - }) - - It("returns error when attempting to use non-existent connection", func() { - // Try to send a message to a non-existent connection - textReq := &websocket.SendTextRequest{ - ConnectionId: "non-existent-connection", - Message: "this should fail", - } - - sendResp, err := wsService.sendText(ctx, pluginName, textReq) - Expect(err).ToNot(HaveOccurred()) - Expect(sendResp.Error).To(ContainSubstring("connection not found")) - - // Try to close a non-existent connection - closeReq := &websocket.CloseRequest{ - ConnectionId: "non-existent-connection", - Code: 1000, - Reason: "test complete", - } - - closeResp, err := wsService.close(ctx, pluginName, closeReq) - Expect(err).ToNot(HaveOccurred()) - Expect(closeResp.Error).To(ContainSubstring("connection not found")) - }) - }) -}) diff --git a/plugins/manager.go b/plugins/manager.go deleted file mode 100644 index 35a1130fd..000000000 --- a/plugins/manager.go +++ /dev/null @@ -1,421 +0,0 @@ -package plugins - -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative api/api.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/http/http.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/config/config.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/websocket/websocket.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/scheduler/scheduler.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/cache/cache.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/artwork/artwork.proto -//go:generate protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative host/subsonicapi/subsonicapi.proto - -import ( - "fmt" - "net/http" - "os" - "slices" - "sync" - "sync/atomic" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/core/agents" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/core/scrobbler" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/schema" - "github.com/navidrome/navidrome/utils/singleton" - "github.com/navidrome/navidrome/utils/slice" - "github.com/tetratelabs/wazero" -) - -const ( - CapabilityMetadataAgent = "MetadataAgent" - CapabilityScrobbler = "Scrobbler" - CapabilitySchedulerCallback = "SchedulerCallback" - CapabilityWebSocketCallback = "WebSocketCallback" - CapabilityLifecycleManagement = "LifecycleManagement" -) - -// pluginCreators maps capability types to their respective creator functions -type pluginConstructor func(wasmPath, pluginID string, m *managerImpl, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin - -var pluginCreators = map[string]pluginConstructor{ - CapabilityMetadataAgent: newWasmMediaAgent, - CapabilityScrobbler: newWasmScrobblerPlugin, - CapabilitySchedulerCallback: newWasmSchedulerCallback, - CapabilityWebSocketCallback: newWasmWebSocketCallback, -} - -// WasmPlugin is the base interface that all WASM plugins implement -type WasmPlugin interface { - // PluginID returns the unique identifier of the plugin (folder name) - PluginID() string -} - -type plugin struct { - ID string - Path string - Capabilities []string - WasmPath string - Manifest *schema.PluginManifest // Loaded manifest - Runtime api.WazeroNewRuntime - ModConfig wazero.ModuleConfig - compilationReady chan struct{} - compilationErr error -} - -func (p *plugin) waitForCompilation() error { - timeout := pluginCompilationTimeout() - select { - case <-p.compilationReady: - case <-time.After(timeout): - err := fmt.Errorf("timed out waiting for plugin %s to compile", p.ID) - log.Error("Timed out waiting for plugin compilation", "name", p.ID, "path", p.WasmPath, "timeout", timeout, "err", err) - return err - } - if p.compilationErr != nil { - log.Error("Failed to compile plugin", "name", p.ID, "path", p.WasmPath, p.compilationErr) - } - return p.compilationErr -} - -type SubsonicRouter http.Handler - -type Manager interface { - SetSubsonicRouter(router SubsonicRouter) - EnsureCompiled(name string) error - PluginList() map[string]schema.PluginManifest - PluginNames(capability string) []string - LoadPlugin(name string, capability string) WasmPlugin - LoadMediaAgent(name string) (agents.Interface, bool) - LoadScrobbler(name string) (scrobbler.Scrobbler, bool) - ScanPlugins() -} - -// managerImpl is a singleton that manages plugins -type managerImpl struct { - plugins map[string]*plugin // Map of plugin folder name to plugin info - pluginsMu sync.RWMutex // Protects plugins map - subsonicRouter atomic.Pointer[SubsonicRouter] // Subsonic API router - schedulerService *schedulerService // Service for handling scheduled tasks - websocketService *websocketService // Service for handling WebSocket connections - lifecycle *pluginLifecycleManager // Manages plugin lifecycle and initialization - adapters map[string]WasmPlugin // Map of plugin folder name + capability to adapter - ds model.DataStore // DataStore for accessing persistent data - metrics metrics.Metrics -} - -// GetManager returns the singleton instance of managerImpl -func GetManager(ds model.DataStore, metrics metrics.Metrics) Manager { - if !conf.Server.Plugins.Enabled { - return &noopManager{} - } - return singleton.GetInstance(func() *managerImpl { - return createManager(ds, metrics) - }) -} - -// createManager creates a new managerImpl instance. Used in tests -func createManager(ds model.DataStore, metrics metrics.Metrics) *managerImpl { - m := &managerImpl{ - plugins: make(map[string]*plugin), - lifecycle: newPluginLifecycleManager(metrics), - ds: ds, - metrics: metrics, - } - - // Create the host services - m.schedulerService = newSchedulerService(m) - m.websocketService = newWebsocketService(m) - - return m -} - -// SetSubsonicRouter sets the SubsonicRouter after managerImpl initialization -func (m *managerImpl) SetSubsonicRouter(router SubsonicRouter) { - m.subsonicRouter.Store(&router) -} - -// registerPlugin adds a plugin to the registry with the given parameters -// Used internally by ScanPlugins to register plugins -func (m *managerImpl) registerPlugin(pluginID, pluginDir, wasmPath string, manifest *schema.PluginManifest) *plugin { - // Create custom runtime function - customRuntime := m.createRuntime(pluginID, manifest.Permissions) - - // Configure module and determine plugin name - mc := newWazeroModuleConfig() - - // Check if it's a symlink, indicating development mode - isSymlink := false - if fileInfo, err := os.Lstat(pluginDir); err == nil { - isSymlink = fileInfo.Mode()&os.ModeSymlink != 0 - } - - // Store plugin info - p := &plugin{ - ID: pluginID, - Path: pluginDir, - Capabilities: slice.Map(manifest.Capabilities, func(cap schema.PluginManifestCapabilitiesElem) string { return string(cap) }), - WasmPath: wasmPath, - Manifest: manifest, - Runtime: customRuntime, - ModConfig: mc, - compilationReady: make(chan struct{}), - } - - // Register the plugin first - m.pluginsMu.Lock() - m.plugins[pluginID] = p - - // Register one plugin adapter for each capability - for _, capability := range manifest.Capabilities { - capabilityStr := string(capability) - constructor := pluginCreators[capabilityStr] - if constructor == nil { - // Warn about unknown capabilities, except for LifecycleManagement (it does not have an adapter) - if capability != CapabilityLifecycleManagement { - log.Warn("Unknown plugin capability type", "capability", capability, "plugin", pluginID) - } - continue - } - adapter := constructor(wasmPath, pluginID, m, customRuntime, mc) - if adapter == nil { - log.Error("Failed to create plugin adapter", "plugin", pluginID, "capability", capabilityStr, "path", wasmPath) - continue - } - m.adapters[pluginID+"_"+capabilityStr] = adapter - } - m.pluginsMu.Unlock() - - log.Info("Discovered plugin", "folder", pluginID, "name", manifest.Name, "capabilities", manifest.Capabilities, "wasm", wasmPath, "dev_mode", isSymlink) - return m.plugins[pluginID] -} - -// initializePluginIfNeeded calls OnInit on plugins that implement LifecycleManagement -func (m *managerImpl) initializePluginIfNeeded(plugin *plugin) { - // Skip if already initialized - if m.lifecycle.isInitialized(plugin) { - return - } - - // Check if the plugin implements LifecycleManagement - if slices.Contains(plugin.Manifest.Capabilities, CapabilityLifecycleManagement) { - if err := m.lifecycle.callOnInit(plugin); err != nil { - m.unregisterPlugin(plugin.ID) - } - } -} - -// unregisterPlugin removes a plugin from the manager -func (m *managerImpl) unregisterPlugin(pluginID string) { - m.pluginsMu.Lock() - defer m.pluginsMu.Unlock() - - plugin, ok := m.plugins[pluginID] - if !ok { - return - } - - // Clear initialization state from lifecycle manager - m.lifecycle.clearInitialized(plugin) - - // Unregister plugin adapters - for _, capability := range plugin.Manifest.Capabilities { - delete(m.adapters, pluginID+"_"+string(capability)) - } - - // Unregister plugin - delete(m.plugins, pluginID) - log.Info("Unregistered plugin", "plugin", pluginID) -} - -// ScanPlugins scans the plugins directory, discovers all valid plugins, and registers them for use. -func (m *managerImpl) ScanPlugins() { - // Clear existing plugins - m.pluginsMu.Lock() - m.plugins = make(map[string]*plugin) - m.adapters = make(map[string]WasmPlugin) - m.pluginsMu.Unlock() - - // Get plugins directory from config - root := conf.Server.Plugins.Folder - log.Debug("Scanning plugins folder", "root", root) - - // Fail fast if the compilation cache cannot be initialized - _, err := getCompilationCache() - if err != nil { - log.Error("Failed to initialize plugins compilation cache. Disabling plugins", err) - return - } - - // Discover all plugins using the shared discovery function - discoveries := DiscoverPlugins(root) - - var validPluginNames []string - var registeredPlugins []*plugin - for _, discovery := range discoveries { - if discovery.Error != nil { - // Handle global errors (like directory read failure) - if discovery.ID == "" { - log.Error("Plugin discovery failed", discovery.Error) - return - } - // Handle individual plugin errors - log.Error("Failed to process plugin", "plugin", discovery.ID, discovery.Error) - continue - } - - // Log discovery details - log.Debug("Processing entry", "name", discovery.ID, "isSymlink", discovery.IsSymlink) - if discovery.IsSymlink { - log.Debug("Processing symlinked plugin directory", "name", discovery.ID, "target", discovery.Path) - } - log.Debug("Checking for plugin.wasm", "wasmPath", discovery.WasmPath) - log.Debug("Manifest loaded successfully", "folder", discovery.ID, "name", discovery.Manifest.Name, "capabilities", discovery.Manifest.Capabilities) - - validPluginNames = append(validPluginNames, discovery.ID) - - // Register the plugin - plugin := m.registerPlugin(discovery.ID, discovery.Path, discovery.WasmPath, discovery.Manifest) - if plugin != nil { - registeredPlugins = append(registeredPlugins, plugin) - } - } - - // Start background processing for all registered plugins after registration is complete - // This avoids race conditions between registration and goroutines that might unregister plugins - for _, p := range registeredPlugins { - go func(plugin *plugin) { - precompilePlugin(plugin) - // Check if this plugin implements InitService and hasn't been initialized yet - m.initializePluginIfNeeded(plugin) - }(p) - } - - log.Debug("Found valid plugins", "count", len(validPluginNames), "plugins", validPluginNames) -} - -// PluginList returns a map of all registered plugins with their manifests -func (m *managerImpl) PluginList() map[string]schema.PluginManifest { - m.pluginsMu.RLock() - defer m.pluginsMu.RUnlock() - - // Create a map to hold the plugin manifests - pluginList := make(map[string]schema.PluginManifest, len(m.plugins)) - for name, plugin := range m.plugins { - // Use the plugin ID as the key and the manifest as the value - pluginList[name] = *plugin.Manifest - } - return pluginList -} - -// PluginNames returns the folder names of all plugins that implement the specified capability -func (m *managerImpl) PluginNames(capability string) []string { - m.pluginsMu.RLock() - defer m.pluginsMu.RUnlock() - - var names []string - for name, plugin := range m.plugins { - for _, c := range plugin.Manifest.Capabilities { - if string(c) == capability { - names = append(names, name) - break - } - } - } - return names -} - -func (m *managerImpl) getPlugin(name string, capability string) (*plugin, WasmPlugin, error) { - m.pluginsMu.RLock() - defer m.pluginsMu.RUnlock() - info, infoOk := m.plugins[name] - adapter, adapterOk := m.adapters[name+"_"+capability] - - if !infoOk { - return nil, nil, fmt.Errorf("plugin not registered: %s", name) - } - if !adapterOk { - return nil, nil, fmt.Errorf("plugin adapter not registered: %s, capability: %s", name, capability) - } - return info, adapter, nil -} - -// LoadPlugin instantiates and returns a plugin by folder name -func (m *managerImpl) LoadPlugin(name string, capability string) WasmPlugin { - info, adapter, err := m.getPlugin(name, capability) - if err != nil { - log.Warn("Error loading plugin", err) - return nil - } - - log.Debug("Loading plugin", "name", name, "path", info.Path) - - // Wait for the plugin to be ready before using it. - if err := info.waitForCompilation(); err != nil { - log.Error("Plugin is not ready, cannot be loaded", "plugin", name, "capability", capability, "err", err) - return nil - } - - if adapter == nil { - log.Warn("Plugin adapter not found", "name", name, "capability", capability) - return nil - } - return adapter -} - -// EnsureCompiled waits for a plugin to finish compilation and returns any compilation error. -// This is useful when you need to wait for compilation without loading a specific capability, -// such as during plugin refresh operations or health checks. -func (m *managerImpl) EnsureCompiled(name string) error { - m.pluginsMu.RLock() - plugin, ok := m.plugins[name] - m.pluginsMu.RUnlock() - - if !ok { - return fmt.Errorf("plugin not found: %s", name) - } - - return plugin.waitForCompilation() -} - -// LoadMediaAgent instantiates and returns a media agent plugin by folder name -func (m *managerImpl) LoadMediaAgent(name string) (agents.Interface, bool) { - plugin := m.LoadPlugin(name, CapabilityMetadataAgent) - if plugin == nil { - return nil, false - } - agent, ok := plugin.(*wasmMediaAgent) - return agent, ok -} - -// LoadScrobbler instantiates and returns a scrobbler plugin by folder name -func (m *managerImpl) LoadScrobbler(name string) (scrobbler.Scrobbler, bool) { - plugin := m.LoadPlugin(name, CapabilityScrobbler) - if plugin == nil { - return nil, false - } - s, ok := plugin.(scrobbler.Scrobbler) - return s, ok -} - -type noopManager struct{} - -func (n noopManager) SetSubsonicRouter(router SubsonicRouter) {} - -func (n noopManager) EnsureCompiled(name string) error { return nil } - -func (n noopManager) PluginList() map[string]schema.PluginManifest { return nil } - -func (n noopManager) PluginNames(capability string) []string { return nil } - -func (n noopManager) LoadPlugin(name string, capability string) WasmPlugin { return nil } - -func (n noopManager) LoadMediaAgent(name string) (agents.Interface, bool) { return nil, false } - -func (n noopManager) LoadScrobbler(name string) (scrobbler.Scrobbler, bool) { return nil, false } - -func (n noopManager) ScanPlugins() {} diff --git a/plugins/manager_test.go b/plugins/manager_test.go deleted file mode 100644 index 8b361f8b3..000000000 --- a/plugins/manager_test.go +++ /dev/null @@ -1,367 +0,0 @@ -package plugins - -import ( - "context" - "os" - "path/filepath" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/core/agents" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Plugin Manager", func() { - var mgr *managerImpl - var ctx context.Context - - BeforeEach(func() { - // We change the plugins folder to random location to avoid conflicts with other tests, - // but, as this is an integration test, we can't use configtest.SetupConfig() as it causes - // data races. - originalPluginsFolder := conf.Server.Plugins.Folder - originalTimeout := conf.Server.DevPluginCompilationTimeout - conf.Server.DevPluginCompilationTimeout = 2 * time.Minute - DeferCleanup(func() { - conf.Server.Plugins.Folder = originalPluginsFolder - conf.Server.DevPluginCompilationTimeout = originalTimeout - }) - conf.Server.Plugins.Enabled = true - conf.Server.Plugins.Folder = testDataDir - - ctx = GinkgoT().Context() - mgr = createManager(nil, metrics.NewNoopInstance()) - mgr.ScanPlugins() - - // Wait for all plugins to compile to avoid race conditions - err := mgr.EnsureCompiled("fake_artist_agent") - Expect(err).NotTo(HaveOccurred(), "fake_artist_agent should compile successfully") - err = mgr.EnsureCompiled("fake_album_agent") - Expect(err).NotTo(HaveOccurred(), "fake_album_agent should compile successfully") - err = mgr.EnsureCompiled("multi_plugin") - Expect(err).NotTo(HaveOccurred(), "multi_plugin should compile successfully") - err = mgr.EnsureCompiled("unauthorized_plugin") - Expect(err).NotTo(HaveOccurred(), "unauthorized_plugin should compile successfully") - }) - - It("should scan and discover plugins from the testdata folder", func() { - Expect(mgr).NotTo(BeNil()) - - mediaAgentNames := mgr.PluginNames("MetadataAgent") - Expect(mediaAgentNames).To(HaveLen(4)) - Expect(mediaAgentNames).To(ContainElements( - "fake_artist_agent", - "fake_album_agent", - "multi_plugin", - "unauthorized_plugin", - )) - - scrobblerNames := mgr.PluginNames("Scrobbler") - Expect(scrobblerNames).To(ContainElement("fake_scrobbler")) - - initServiceNames := mgr.PluginNames("LifecycleManagement") - Expect(initServiceNames).To(ContainElements("multi_plugin", "fake_init_service")) - - schedulerCallbackNames := mgr.PluginNames("SchedulerCallback") - Expect(schedulerCallbackNames).To(ContainElement("multi_plugin")) - }) - - It("should load all plugins from folder", func() { - all := mgr.PluginList() - Expect(all).To(HaveLen(6)) - Expect(all["fake_artist_agent"].Name).To(Equal("fake_artist_agent")) - Expect(all["unauthorized_plugin"].Capabilities).To(HaveExactElements(schema.PluginManifestCapabilitiesElem("MetadataAgent"))) - }) - - It("should load a MetadataAgent plugin and invoke artist-related methods", func() { - plugin := mgr.LoadPlugin("fake_artist_agent", CapabilityMetadataAgent) - Expect(plugin).NotTo(BeNil()) - - agent, ok := plugin.(agents.Interface) - Expect(ok).To(BeTrue(), "plugin should implement agents.Interface") - Expect(agent.AgentName()).To(Equal("fake_artist_agent")) - - mbidRetriever, ok := agent.(agents.ArtistMBIDRetriever) - Expect(ok).To(BeTrue()) - mbid, err := mbidRetriever.GetArtistMBID(ctx, "123", "The Beatles") - Expect(err).NotTo(HaveOccurred()) - Expect(mbid).To(Equal("1234567890")) - }) - - It("should load all MetadataAgent plugins", func() { - mediaAgentNames := mgr.PluginNames("MetadataAgent") - Expect(mediaAgentNames).To(HaveLen(4)) - - var agentNames []string - for _, name := range mediaAgentNames { - agent, ok := mgr.LoadMediaAgent(name) - if ok { - agentNames = append(agentNames, agent.AgentName()) - } - } - - Expect(agentNames).To(ContainElements("fake_artist_agent", "fake_album_agent", "multi_plugin", "unauthorized_plugin")) - }) - - Describe("ScanPlugins", func() { - var tempPluginsDir string - var m *managerImpl - - BeforeEach(func() { - tempPluginsDir, _ = os.MkdirTemp("", "navidrome-plugins-test-*") - DeferCleanup(func() { - _ = os.RemoveAll(tempPluginsDir) - }) - - conf.Server.Plugins.Folder = tempPluginsDir - m = createManager(nil, metrics.NewNoopInstance()) - }) - - // Helper to create a complete valid plugin for manager testing - createValidPlugin := func(folderName, manifestName string) { - pluginDir := filepath.Join(tempPluginsDir, folderName) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Copy real WASM file from testdata - sourceWasmPath := filepath.Join(testDataDir, "fake_artist_agent", "plugin.wasm") - targetWasmPath := filepath.Join(pluginDir, "plugin.wasm") - sourceWasm, err := os.ReadFile(sourceWasmPath) - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(targetWasmPath, sourceWasm, 0600)).To(Succeed()) - - manifest := `{ - "name": "` + manifestName + `", - "version": "1.0.0", - "capabilities": ["MetadataAgent"], - "author": "Test Author", - "description": "Test Plugin", - "website": "https://test.navidrome.org/` + manifestName + `", - "permissions": {} - }` - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifest), 0600)).To(Succeed()) - } - - It("should register and compile discovered plugins", func() { - createValidPlugin("test-plugin", "test-plugin") - - m.ScanPlugins() - - // Focus on manager behavior: registration and compilation - Expect(m.plugins).To(HaveLen(1)) - Expect(m.plugins).To(HaveKey("test-plugin")) - - plugin := m.plugins["test-plugin"] - Expect(plugin.ID).To(Equal("test-plugin")) - Expect(plugin.Manifest.Name).To(Equal("test-plugin")) - - // Verify plugin can be loaded (compilation successful) - loadedPlugin := m.LoadPlugin("test-plugin", CapabilityMetadataAgent) - Expect(loadedPlugin).NotTo(BeNil()) - }) - - It("should handle multiple plugins with different IDs but same manifest names", func() { - // This tests manager-specific behavior: how it handles ID conflicts - createValidPlugin("lastfm-official", "lastfm") - createValidPlugin("lastfm-custom", "lastfm") - - m.ScanPlugins() - - // Both should be registered with their folder names as IDs - Expect(m.plugins).To(HaveLen(2)) - Expect(m.plugins).To(HaveKey("lastfm-official")) - Expect(m.plugins).To(HaveKey("lastfm-custom")) - - // Both should be loadable independently - official := m.LoadPlugin("lastfm-official", CapabilityMetadataAgent) - custom := m.LoadPlugin("lastfm-custom", CapabilityMetadataAgent) - Expect(official).NotTo(BeNil()) - Expect(custom).NotTo(BeNil()) - Expect(official.PluginID()).To(Equal("lastfm-official")) - Expect(custom.PluginID()).To(Equal("lastfm-custom")) - }) - }) - - Describe("LoadPlugin", func() { - It("should load a MetadataAgent plugin and invoke artist-related methods", func() { - plugin := mgr.LoadPlugin("fake_artist_agent", CapabilityMetadataAgent) - Expect(plugin).NotTo(BeNil()) - - agent, ok := plugin.(agents.Interface) - Expect(ok).To(BeTrue(), "plugin should implement agents.Interface") - Expect(agent.AgentName()).To(Equal("fake_artist_agent")) - - mbidRetriever, ok := agent.(agents.ArtistMBIDRetriever) - Expect(ok).To(BeTrue()) - mbid, err := mbidRetriever.GetArtistMBID(ctx, "id", "Test Artist") - Expect(err).NotTo(HaveOccurred()) - Expect(mbid).To(Equal("1234567890")) - }) - }) - - Describe("EnsureCompiled", func() { - It("should successfully wait for plugin compilation", func() { - err := mgr.EnsureCompiled("fake_artist_agent") - Expect(err).NotTo(HaveOccurred()) - }) - - It("should return error for non-existent plugin", func() { - err := mgr.EnsureCompiled("non-existent-plugin") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("plugin not found: non-existent-plugin")) - }) - - It("should wait for compilation to complete for all valid plugins", func() { - pluginNames := []string{"fake_artist_agent", "fake_album_agent", "multi_plugin", "fake_scrobbler"} - - for _, name := range pluginNames { - err := mgr.EnsureCompiled(name) - Expect(err).NotTo(HaveOccurred(), "plugin %s should compile successfully", name) - } - }) - }) - - Describe("Invoke Methods", func() { - It("should load all MetadataAgent plugins and invoke methods", func() { - fakeAlbumPlugin, isMediaAgent := mgr.LoadMediaAgent("fake_album_agent") - Expect(isMediaAgent).To(BeTrue()) - - Expect(fakeAlbumPlugin).NotTo(BeNil(), "fake_album_agent should be loaded") - - // Test GetAlbumInfo method - need to cast to the specific interface - albumRetriever, ok := fakeAlbumPlugin.(agents.AlbumInfoRetriever) - Expect(ok).To(BeTrue(), "fake_album_agent should implement AlbumInfoRetriever") - - info, err := albumRetriever.GetAlbumInfo(ctx, "Test Album", "Test Artist", "123") - Expect(err).NotTo(HaveOccurred()) - Expect(info).NotTo(BeNil()) - Expect(info.Name).To(Equal("Test Album")) - }) - }) - - Describe("Permission Enforcement Integration", func() { - It("should fail when plugin tries to access unauthorized services", func() { - // This plugin tries to access config service but has no permissions - plugin := mgr.LoadPlugin("unauthorized_plugin", CapabilityMetadataAgent) - Expect(plugin).NotTo(BeNil()) - - agent, ok := plugin.(agents.Interface) - Expect(ok).To(BeTrue()) - - // This should fail because the plugin tries to access unauthorized config service - // The exact behavior depends on the plugin implementation, but it should either: - // 1. Fail during instantiation, or - // 2. Return an error when trying to call config methods - - // Try to use one of the available methods - let's test with GetArtistMBID - mbidRetriever, isMBIDRetriever := agent.(agents.ArtistMBIDRetriever) - if isMBIDRetriever { - _, err := mbidRetriever.GetArtistMBID(ctx, "id", "Test Artist") - if err == nil { - // If no error, the plugin should still be working - // but any config access should fail silently or return default values - Expect(agent.AgentName()).To(Equal("unauthorized_plugin")) - } else { - // If there's an error, it should be related to missing permissions - Expect(err.Error()).To(ContainSubstring("")) - } - } else { - // If the plugin doesn't implement the interface, that's also acceptable - Expect(agent.AgentName()).To(Equal("unauthorized_plugin")) - } - }) - }) - - Describe("Plugin Initialization Lifecycle", func() { - BeforeEach(func() { - conf.Server.Plugins.Enabled = true - conf.Server.Plugins.Folder = testDataDir - }) - - Context("when OnInit is successful", func() { - It("should register and initialize the plugin", func() { - conf.Server.PluginConfig = nil - mgr = createManager(nil, metrics.NewNoopInstance()) // Create manager after setting config - mgr.ScanPlugins() - - plugin := mgr.plugins["fake_init_service"] - Expect(plugin).NotTo(BeNil()) - - Eventually(func() bool { - return mgr.lifecycle.isInitialized(plugin) - }).Should(BeTrue()) - - // Check that the plugin is still registered - names := mgr.PluginNames(CapabilityLifecycleManagement) - Expect(names).To(ContainElement("fake_init_service")) - }) - }) - - Context("when OnInit fails", func() { - It("should unregister the plugin if OnInit returns an error string", func() { - conf.Server.PluginConfig = map[string]map[string]string{ - "fake_init_service": { - "returnError": "response_error", - }, - } - mgr = createManager(nil, metrics.NewNoopInstance()) // Create manager after setting config - mgr.ScanPlugins() - - Eventually(func() []string { - return mgr.PluginNames(CapabilityLifecycleManagement) - }).ShouldNot(ContainElement("fake_init_service")) - }) - - It("should unregister the plugin if OnInit returns a Go error", func() { - conf.Server.PluginConfig = map[string]map[string]string{ - "fake_init_service": { - "returnError": "go_error", - }, - } - mgr = createManager(nil, metrics.NewNoopInstance()) // Create manager after setting config - mgr.ScanPlugins() - - Eventually(func() []string { - return mgr.PluginNames(CapabilityLifecycleManagement) - }).ShouldNot(ContainElement("fake_init_service")) - }) - }) - - It("should clear lifecycle state when unregistering a plugin", func() { - // Create a manager and register a plugin - mgr := createManager(nil, metrics.NewNoopInstance()) - - // Create a mock plugin with LifecycleManagement capability - plugin := &plugin{ - ID: "test-plugin", - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - // Register the plugin in the manager - mgr.pluginsMu.Lock() - mgr.plugins[plugin.ID] = plugin - mgr.pluginsMu.Unlock() - - // Mark the plugin as initialized in the lifecycle manager - mgr.lifecycle.markInitialized(plugin) - Expect(mgr.lifecycle.isInitialized(plugin)).To(BeTrue()) - - // Unregister the plugin - mgr.unregisterPlugin(plugin.ID) - - // Verify that the plugin is no longer in the manager - mgr.pluginsMu.RLock() - _, exists := mgr.plugins[plugin.ID] - mgr.pluginsMu.RUnlock() - Expect(exists).To(BeFalse()) - - // Verify that the lifecycle state has been cleared - Expect(mgr.lifecycle.isInitialized(plugin)).To(BeFalse()) - }) - }) -}) diff --git a/plugins/manifest.go b/plugins/manifest.go deleted file mode 100644 index b56187bcc..000000000 --- a/plugins/manifest.go +++ /dev/null @@ -1,30 +0,0 @@ -package plugins - -//go:generate go tool go-jsonschema --schema-root-type navidrome://plugins/manifest=PluginManifest -p schema --output schema/manifest_gen.go schema/manifest.schema.json - -import ( - _ "embed" - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/navidrome/navidrome/plugins/schema" -) - -// LoadManifest loads and parses the manifest.json file from the given plugin directory. -// Returns the generated schema.PluginManifest type with full validation and type safety. -func LoadManifest(pluginDir string) (*schema.PluginManifest, error) { - manifestPath := filepath.Join(pluginDir, "manifest.json") - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("failed to read manifest file: %w", err) - } - - var manifest schema.PluginManifest - if err := json.Unmarshal(data, &manifest); err != nil { - return nil, fmt.Errorf("invalid manifest: %w", err) - } - - return &manifest, nil -} diff --git a/plugins/manifest_permissions_test.go b/plugins/manifest_permissions_test.go deleted file mode 100644 index 7a3df5f2d..000000000 --- a/plugins/manifest_permissions_test.go +++ /dev/null @@ -1,526 +0,0 @@ -package plugins - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/conf/configtest" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -// Helper function to create test plugins with typed permissions -func createTestPlugin(tempDir, name string, permissions schema.PluginManifestPermissions) string { - pluginDir := filepath.Join(tempDir, name) - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - // Use the generated PluginManifest type directly - it handles JSON marshaling automatically - manifest := schema.PluginManifest{ - Name: name, - Author: "Test Author", - Version: "1.0.0", - Description: "Test plugin for permissions", - Website: "https://test.navidrome.org/" + name, - Capabilities: []schema.PluginManifestCapabilitiesElem{ - schema.PluginManifestCapabilitiesElemMetadataAgent, - }, - Permissions: permissions, - } - - // Marshal the typed manifest directly - gets all validation for free - manifestData, err := json.Marshal(manifest) - Expect(err).NotTo(HaveOccurred()) - - manifestPath := filepath.Join(pluginDir, "manifest.json") - Expect(os.WriteFile(manifestPath, manifestData, 0600)).To(Succeed()) - - // Create fake WASM file (since plugin discovery checks for it) - wasmPath := filepath.Join(pluginDir, "plugin.wasm") - Expect(os.WriteFile(wasmPath, []byte("fake wasm content"), 0600)).To(Succeed()) - - return pluginDir -} - -var _ = Describe("Plugin Permissions", func() { - var ( - mgr *managerImpl - tempDir string - ctx context.Context - ) - - BeforeEach(func() { - DeferCleanup(configtest.SetupConfig()) - ctx = context.Background() - mgr = createManager(nil, metrics.NewNoopInstance()) - tempDir = GinkgoT().TempDir() - }) - - Describe("Permission Enforcement in createRuntime", func() { - It("should only load services specified in permissions", func() { - // Test with limited permissions using typed structs - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration settings", - }, - } - - runtimeFunc := mgr.createRuntime("test-plugin", permissions) - - // Create runtime to test service availability - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - // The runtime was created successfully with the specified permissions - Expect(runtime).NotTo(BeNil()) - - // Note: The actual verification of which specific host functions are available - // would require introspecting the WASM runtime, which is complex. - // The key test is that the runtime creation succeeds with valid permissions. - }) - - It("should create runtime with empty permissions", func() { - permissions := schema.PluginManifestPermissions{} - - runtimeFunc := mgr.createRuntime("empty-permissions-plugin", permissions) - - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - // Should succeed but with no host services available - Expect(runtime).NotTo(BeNil()) - }) - - It("should handle all available permissions", func() { - // Test with all possible permissions using typed structs - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration settings", - }, - Scheduler: &schema.PluginManifestPermissionsScheduler{ - Reason: "To schedule periodic tasks", - }, - Websocket: &schema.PluginManifestPermissionsWebsocket{ - Reason: "To handle real-time communication", - AllowedUrls: []string{"wss://api.example.com"}, - AllowLocalNetwork: false, - }, - Cache: &schema.PluginManifestPermissionsCache{ - Reason: "To cache data and reduce API calls", - }, - Artwork: &schema.PluginManifestPermissionsArtwork{ - Reason: "To generate artwork URLs", - }, - } - - runtimeFunc := mgr.createRuntime("full-permissions-plugin", permissions) - - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - Expect(runtime).NotTo(BeNil()) - }) - }) - - Describe("Plugin Discovery with Permissions", func() { - BeforeEach(func() { - conf.Server.Plugins.Folder = tempDir - }) - - It("should discover plugin with valid permissions manifest", func() { - // Create plugin with http permission using typed structs - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch metadata from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - }, - } - createTestPlugin(tempDir, "valid-plugin", permissions) - - // Scan for plugins - mgr.ScanPlugins() - - // Verify plugin was discovered (even without valid WASM) - pluginNames := mgr.PluginNames("MetadataAgent") - Expect(pluginNames).To(ContainElement("valid-plugin")) - }) - - It("should discover plugin with no permissions", func() { - // Create plugin with empty permissions using typed structs - permissions := schema.PluginManifestPermissions{} - createTestPlugin(tempDir, "no-perms-plugin", permissions) - - mgr.ScanPlugins() - - pluginNames := mgr.PluginNames("MetadataAgent") - Expect(pluginNames).To(ContainElement("no-perms-plugin")) - }) - - It("should discover plugin with multiple permissions", func() { - // Create plugin with multiple permissions using typed structs - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch metadata from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read plugin configuration settings", - }, - Scheduler: &schema.PluginManifestPermissionsScheduler{ - Reason: "To schedule periodic data updates", - }, - } - createTestPlugin(tempDir, "multi-perms-plugin", permissions) - - mgr.ScanPlugins() - - pluginNames := mgr.PluginNames("MetadataAgent") - Expect(pluginNames).To(ContainElement("multi-perms-plugin")) - }) - }) - - Describe("Existing Plugin Permissions", func() { - BeforeEach(func() { - // Use the testdata directory with updated plugins - conf.Server.Plugins.Folder = testDataDir - mgr.ScanPlugins() - }) - - It("should discover fake_scrobbler with empty permissions", func() { - scrobblerNames := mgr.PluginNames(CapabilityScrobbler) - Expect(scrobblerNames).To(ContainElement("fake_scrobbler")) - }) - - It("should discover multi_plugin with scheduler permissions", func() { - agentNames := mgr.PluginNames(CapabilityMetadataAgent) - Expect(agentNames).To(ContainElement("multi_plugin")) - }) - - It("should discover all test plugins successfully", func() { - // All test plugins should be discovered with their updated permissions - testPlugins := []struct { - name string - capability string - }{ - {"fake_album_agent", CapabilityMetadataAgent}, - {"fake_artist_agent", CapabilityMetadataAgent}, - {"fake_scrobbler", CapabilityScrobbler}, - {"multi_plugin", CapabilityMetadataAgent}, - {"fake_init_service", CapabilityLifecycleManagement}, - } - - for _, testPlugin := range testPlugins { - pluginNames := mgr.PluginNames(testPlugin.capability) - Expect(pluginNames).To(ContainElement(testPlugin.name), "Plugin %s should be discovered", testPlugin.name) - } - }) - }) - - Describe("Permission Validation", func() { - It("should enforce permissions are required in manifest", func() { - // Create a manifest JSON string without the permissions field - manifestContent := `{ - "name": "test-plugin", - "author": "Test Author", - "version": "1.0.0", - "description": "A test plugin", - "website": "https://test.navidrome.org/test-plugin", - "capabilities": ["MetadataAgent"] - }` - - manifestPath := filepath.Join(tempDir, "manifest.json") - err := os.WriteFile(manifestPath, []byte(manifestContent), 0600) - Expect(err).NotTo(HaveOccurred()) - - _, err = LoadManifest(tempDir) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("field permissions in PluginManifest: required")) - }) - - It("should allow unknown permission keys", func() { - // Create manifest with both known and unknown permission types - pluginDir := filepath.Join(tempDir, "unknown-perms") - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - manifestContent := `{ - "name": "unknown-perms", - "author": "Test Author", - "version": "1.0.0", - "description": "Manifest with unknown permissions", - "website": "https://test.navidrome.org/unknown-perms", - "capabilities": ["MetadataAgent"], - "permissions": { - "http": { - "reason": "To fetch data from external APIs", - "allowedUrls": { - "*": ["*"] - } - }, - "unknown": { - "customField": "customValue" - } - } - }` - - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifestContent), 0600)).To(Succeed()) - - // Test manifest loading directly - should succeed even with unknown permissions - loadedManifest, err := LoadManifest(pluginDir) - Expect(err).NotTo(HaveOccurred()) - Expect(loadedManifest).NotTo(BeNil()) - // With typed permissions, we check the specific fields - Expect(loadedManifest.Permissions.Http).NotTo(BeNil()) - Expect(loadedManifest.Permissions.Http.Reason).To(Equal("To fetch data from external APIs")) - // The key point is that the manifest loads successfully despite unknown permissions - // The actual handling of AdditionalProperties depends on the JSON schema implementation - }) - }) - - Describe("Runtime Pool with Permissions", func() { - It("should create separate runtimes for different permission sets", func() { - // Create two different permission sets using typed structs - permissions1 := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - } - permissions2 := schema.PluginManifestPermissions{ - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration settings", - }, - } - - runtimeFunc1 := mgr.createRuntime("plugin1", permissions1) - runtimeFunc2 := mgr.createRuntime("plugin2", permissions2) - - runtime1, err1 := runtimeFunc1(ctx) - Expect(err1).NotTo(HaveOccurred()) - defer runtime1.Close(ctx) - - runtime2, err2 := runtimeFunc2(ctx) - Expect(err2).NotTo(HaveOccurred()) - defer runtime2.Close(ctx) - - // Should be different runtime instances - Expect(runtime1).NotTo(BeIdenticalTo(runtime2)) - }) - }) - - Describe("Permission System Integration", func() { - It("should successfully validate manifests with permissions", func() { - // Create a valid manifest with permissions - pluginDir := filepath.Join(tempDir, "valid-manifest") - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - - manifestContent := `{ - "name": "valid-manifest", - "author": "Test Author", - "version": "1.0.0", - "description": "Valid manifest with permissions", - "website": "https://test.navidrome.org/valid-manifest", - "capabilities": ["MetadataAgent"], - "permissions": { - "http": { - "reason": "To fetch metadata from external APIs", - "allowedUrls": { - "*": ["*"] - } - }, - "config": { - "reason": "To read plugin configuration settings" - } - } - }` - - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifestContent), 0600)).To(Succeed()) - - // Load the manifest - should succeed - manifest, err := LoadManifest(pluginDir) - Expect(err).NotTo(HaveOccurred()) - Expect(manifest).NotTo(BeNil()) - // With typed permissions, check the specific permission fields - Expect(manifest.Permissions.Http).NotTo(BeNil()) - Expect(manifest.Permissions.Http.Reason).To(Equal("To fetch metadata from external APIs")) - Expect(manifest.Permissions.Config).NotTo(BeNil()) - Expect(manifest.Permissions.Config.Reason).To(Equal("To read plugin configuration settings")) - }) - - It("should track which services are requested per plugin", func() { - // Test that different plugins can have different permission sets - permissions1 := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration settings", - }, - } - permissions2 := schema.PluginManifestPermissions{ - Scheduler: &schema.PluginManifestPermissionsScheduler{ - Reason: "To schedule periodic tasks", - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration for scheduler", - }, - } - permissions3 := schema.PluginManifestPermissions{} // Empty permissions - - createTestPlugin(tempDir, "plugin-with-http", permissions1) - createTestPlugin(tempDir, "plugin-with-scheduler", permissions2) - createTestPlugin(tempDir, "plugin-with-none", permissions3) - - conf.Server.Plugins.Folder = tempDir - mgr.ScanPlugins() - - // All should be discovered - pluginNames := mgr.PluginNames(CapabilityMetadataAgent) - Expect(pluginNames).To(ContainElement("plugin-with-http")) - Expect(pluginNames).To(ContainElement("plugin-with-scheduler")) - Expect(pluginNames).To(ContainElement("plugin-with-none")) - }) - }) - - Describe("Runtime Service Access Control", func() { - It("should successfully create runtime with permitted services", func() { - // Create runtime with HTTP permission using typed struct - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - } - - runtimeFunc := mgr.createRuntime("http-only-plugin", permissions) - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - // Runtime should be created successfully - host functions are loaded during runtime creation - Expect(runtime).NotTo(BeNil()) - }) - - It("should successfully create runtime with multiple permitted services", func() { - // Create runtime with multiple permissions using typed structs - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration settings", - }, - Scheduler: &schema.PluginManifestPermissionsScheduler{ - Reason: "To schedule periodic tasks", - }, - } - - runtimeFunc := mgr.createRuntime("multi-service-plugin", permissions) - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - // Runtime should be created successfully - Expect(runtime).NotTo(BeNil()) - }) - - It("should create runtime with no services when no permissions granted", func() { - // Create runtime with empty permissions using typed struct - emptyPermissions := schema.PluginManifestPermissions{} - - runtimeFunc := mgr.createRuntime("no-service-plugin", emptyPermissions) - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - // Runtime should still be created, but with no host services - Expect(runtime).NotTo(BeNil()) - }) - - It("should demonstrate secure-by-default behavior", func() { - // Test that default (empty permissions) provides no services - defaultPermissions := schema.PluginManifestPermissions{} - runtimeFunc := mgr.createRuntime("default-plugin", defaultPermissions) - runtime, err := runtimeFunc(ctx) - Expect(err).NotTo(HaveOccurred()) - defer runtime.Close(ctx) - - // Runtime should be created but with no host services - Expect(runtime).NotTo(BeNil()) - }) - - It("should test permission enforcement by simulating unauthorized service access", func() { - // This test demonstrates that plugins would fail at runtime when trying to call - // host functions they don't have permission for, since those functions are simply - // not loaded into the WASM runtime environment. - - // Create two different runtimes with different permissions using typed structs - httpOnlyPermissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "To fetch data from external APIs", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - } - configOnlyPermissions := schema.PluginManifestPermissions{ - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "To read configuration settings", - }, - } - - httpRuntime, err := mgr.createRuntime("http-only", httpOnlyPermissions)(ctx) - Expect(err).NotTo(HaveOccurred()) - defer httpRuntime.Close(ctx) - - configRuntime, err := mgr.createRuntime("config-only", configOnlyPermissions)(ctx) - Expect(err).NotTo(HaveOccurred()) - defer configRuntime.Close(ctx) - - // Both runtimes should be created successfully, but they will have different - // sets of host functions available. A plugin trying to call unauthorized - // functions would get "function not found" errors during instantiation or execution. - Expect(httpRuntime).NotTo(BeNil()) - Expect(configRuntime).NotTo(BeNil()) - }) - }) -}) diff --git a/plugins/manifest_test.go b/plugins/manifest_test.go deleted file mode 100644 index 2ec3edd19..000000000 --- a/plugins/manifest_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package plugins - -import ( - "os" - "path/filepath" - - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Plugin Manifest", func() { - var tempDir string - - BeforeEach(func() { - tempDir = GinkgoT().TempDir() - }) - - It("should load and parse a valid manifest", func() { - manifestPath := filepath.Join(tempDir, "manifest.json") - manifestContent := []byte(`{ - "name": "test-plugin", - "author": "Test Author", - "version": "1.0.0", - "description": "A test plugin", - "website": "https://test.navidrome.org/test-plugin", - "capabilities": ["MetadataAgent", "Scrobbler"], - "permissions": { - "http": { - "reason": "To fetch metadata", - "allowedUrls": { - "https://api.example.com/*": ["GET"] - } - } - } - }`) - - err := os.WriteFile(manifestPath, manifestContent, 0600) - Expect(err).NotTo(HaveOccurred()) - - manifest, err := LoadManifest(tempDir) - Expect(err).NotTo(HaveOccurred()) - Expect(manifest).NotTo(BeNil()) - Expect(manifest.Name).To(Equal("test-plugin")) - Expect(manifest.Author).To(Equal("Test Author")) - Expect(manifest.Version).To(Equal("1.0.0")) - Expect(manifest.Description).To(Equal("A test plugin")) - Expect(manifest.Capabilities).To(HaveLen(2)) - Expect(manifest.Capabilities[0]).To(Equal(schema.PluginManifestCapabilitiesElemMetadataAgent)) - Expect(manifest.Capabilities[1]).To(Equal(schema.PluginManifestCapabilitiesElemScrobbler)) - Expect(manifest.Permissions.Http).NotTo(BeNil()) - Expect(manifest.Permissions.Http.Reason).To(Equal("To fetch metadata")) - }) - - It("should fail with proper error for non-existent manifest", func() { - _, err := LoadManifest(filepath.Join(tempDir, "non-existent")) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to read manifest file")) - }) - - It("should fail with JSON parse error for invalid JSON", func() { - // Create invalid JSON - invalidJSON := `{ - "name": "test-plugin", - "author": "Test Author" - "version": "1.0.0" - "description": "A test plugin", - "capabilities": ["MetadataAgent"], - "permissions": {} - }` - - pluginDir := filepath.Join(tempDir, "invalid-json") - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(invalidJSON), 0600)).To(Succeed()) - - // Test validation fails - _, err := LoadManifest(pluginDir) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("invalid manifest")) - }) - - It("should validate manifest against schema with detailed error for missing required field", func() { - // Create manifest missing required name field - manifestContent := `{ - "author": "Test Author", - "version": "1.0.0", - "description": "A test plugin", - "website": "https://test.navidrome.org/test-plugin", - "capabilities": ["MetadataAgent"], - "permissions": {} - }` - - pluginDir := filepath.Join(tempDir, "test-plugin") - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifestContent), 0600)).To(Succeed()) - - _, err := LoadManifest(pluginDir) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("field name in PluginManifest: required")) - }) - - It("should validate manifest with wrong capability type", func() { - // Create manifest with invalid capability - manifestContent := `{ - "name": "test-plugin", - "author": "Test Author", - "version": "1.0.0", - "description": "A test plugin", - "website": "https://test.navidrome.org/test-plugin", - "capabilities": ["UnsupportedService"], - "permissions": {} - }` - - pluginDir := filepath.Join(tempDir, "test-plugin") - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifestContent), 0600)).To(Succeed()) - - _, err := LoadManifest(pluginDir) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("invalid value")) - Expect(err.Error()).To(ContainSubstring("UnsupportedService")) - }) - - It("should validate manifest with empty capabilities array", func() { - // Create manifest with empty capabilities array - manifestContent := `{ - "name": "test-plugin", - "author": "Test Author", - "version": "1.0.0", - "description": "A test plugin", - "website": "https://test.navidrome.org/test-plugin", - "capabilities": [], - "permissions": {} - }` - - pluginDir := filepath.Join(tempDir, "test-plugin") - Expect(os.MkdirAll(pluginDir, 0755)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(pluginDir, "manifest.json"), []byte(manifestContent), 0600)).To(Succeed()) - - _, err := LoadManifest(pluginDir) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("field capabilities length: must be >= 1")) - }) -}) diff --git a/plugins/package.go b/plugins/package.go deleted file mode 100644 index 5273b0431..000000000 --- a/plugins/package.go +++ /dev/null @@ -1,177 +0,0 @@ -package plugins - -import ( - "archive/zip" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/navidrome/navidrome/plugins/schema" -) - -// PluginPackage represents a Navidrome Plugin Package (.ndp file) -type PluginPackage struct { - ManifestJSON []byte - Manifest *schema.PluginManifest - WasmBytes []byte - Docs map[string][]byte -} - -// ExtractPackage extracts a .ndp file to the target directory -func ExtractPackage(ndpPath, targetDir string) error { - r, err := zip.OpenReader(ndpPath) - if err != nil { - return fmt.Errorf("error opening .ndp file: %w", err) - } - defer r.Close() - - // Create target directory if it doesn't exist - if err := os.MkdirAll(targetDir, 0755); err != nil { - return fmt.Errorf("error creating plugin directory: %w", err) - } - - // Define a reasonable size limit for plugin files to prevent decompression bombs - const maxFileSize = 10 * 1024 * 1024 // 10 MB limit - - // Extract all files from the zip - for _, f := range r.File { - // Skip directories (they will be created as needed) - if f.FileInfo().IsDir() { - continue - } - - // Create the file path for extraction - // Validate the file name to prevent directory traversal or absolute paths - if strings.Contains(f.Name, "..") || filepath.IsAbs(f.Name) { - return fmt.Errorf("illegal file path in plugin package: %s", f.Name) - } - - // Create the file path for extraction - targetPath := filepath.Join(targetDir, f.Name) // #nosec G305 - - // Clean the path to prevent directory traversal. - cleanedPath := filepath.Clean(targetPath) - // Ensure the cleaned path is still within the target directory. - // We resolve both paths to absolute paths to be sure. - absTargetDir, err := filepath.Abs(targetDir) - if err != nil { - return fmt.Errorf("failed to resolve target directory path: %w", err) - } - absTargetPath, err := filepath.Abs(cleanedPath) - if err != nil { - return fmt.Errorf("failed to resolve extracted file path: %w", err) - } - if !strings.HasPrefix(absTargetPath, absTargetDir+string(os.PathSeparator)) && absTargetPath != absTargetDir { - return fmt.Errorf("illegal file path in plugin package: %s", f.Name) - } - - // Open the file inside the zip - rc, err := f.Open() - if err != nil { - return fmt.Errorf("error opening file in plugin package: %w", err) - } - - // Create parent directories if they don't exist - if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { - rc.Close() - return fmt.Errorf("error creating directory structure: %w", err) - } - - // Create the file - outFile, err := os.Create(targetPath) - if err != nil { - rc.Close() - return fmt.Errorf("error creating extracted file: %w", err) - } - - // Copy the file contents with size limit - if _, err := io.CopyN(outFile, rc, maxFileSize); err != nil && !errors.Is(err, io.EOF) { - outFile.Close() - rc.Close() - if errors.Is(err, io.ErrUnexpectedEOF) { // File size exceeds limit - return fmt.Errorf("error extracting file: size exceeds limit (%d bytes) for %s", maxFileSize, f.Name) - } - return fmt.Errorf("error writing extracted file: %w", err) - } - - outFile.Close() - rc.Close() - - // Set appropriate file permissions (0600 - readable only by owner) - if err := os.Chmod(targetPath, 0600); err != nil { - return fmt.Errorf("error setting permissions on extracted file: %w", err) - } - } - - return nil -} - -// LoadPackage loads and validates an .ndp file without extracting it -func LoadPackage(ndpPath string) (*PluginPackage, error) { - r, err := zip.OpenReader(ndpPath) - if err != nil { - return nil, fmt.Errorf("error opening .ndp file: %w", err) - } - defer r.Close() - - pkg := &PluginPackage{ - Docs: make(map[string][]byte), - } - - // Required files - var hasManifest, hasWasm bool - - // Read all files in the zip - for _, f := range r.File { - // Skip directories - if f.FileInfo().IsDir() { - continue - } - - // Get file content - rc, err := f.Open() - if err != nil { - return nil, fmt.Errorf("error opening file in plugin package: %w", err) - } - - content, err := io.ReadAll(rc) - rc.Close() - if err != nil { - return nil, fmt.Errorf("error reading file in plugin package: %w", err) - } - - // Process based on file name - switch strings.ToLower(f.Name) { - case "manifest.json": - pkg.ManifestJSON = content - hasManifest = true - case "plugin.wasm": - pkg.WasmBytes = content - hasWasm = true - default: - // Store other files as documentation - pkg.Docs[f.Name] = content - } - } - - // Ensure required files exist - if !hasManifest { - return nil, fmt.Errorf("plugin package missing required manifest.json") - } - if !hasWasm { - return nil, fmt.Errorf("plugin package missing required plugin.wasm") - } - - // Parse and validate the manifest - var manifest schema.PluginManifest - if err := json.Unmarshal(pkg.ManifestJSON, &manifest); err != nil { - return nil, fmt.Errorf("invalid manifest: %w", err) - } - - pkg.Manifest = &manifest - return pkg, nil -} diff --git a/plugins/package_test.go b/plugins/package_test.go deleted file mode 100644 index 8ff4b354a..000000000 --- a/plugins/package_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package plugins - -import ( - "archive/zip" - "os" - "path/filepath" - - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Plugin Package", func() { - var tempDir string - var ndpPath string - - BeforeEach(func() { - tempDir = GinkgoT().TempDir() - - // Create a test .ndp file - ndpPath = filepath.Join(tempDir, "test-plugin.ndp") - - // Create the required plugin files - manifestContent := []byte(`{ - "name": "test-plugin", - "author": "Test Author", - "version": "1.0.0", - "description": "A test plugin", - "website": "https://test.navidrome.org/test-plugin", - "capabilities": ["MetadataAgent"], - "permissions": {} - }`) - - wasmContent := []byte("dummy wasm content") - readmeContent := []byte("# Test Plugin\nThis is a test plugin") - - // Create the zip file - zipFile, err := os.Create(ndpPath) - Expect(err).NotTo(HaveOccurred()) - defer zipFile.Close() - - zipWriter := zip.NewWriter(zipFile) - defer zipWriter.Close() - - // Add manifest.json - manifestWriter, err := zipWriter.Create("manifest.json") - Expect(err).NotTo(HaveOccurred()) - _, err = manifestWriter.Write(manifestContent) - Expect(err).NotTo(HaveOccurred()) - - // Add plugin.wasm - wasmWriter, err := zipWriter.Create("plugin.wasm") - Expect(err).NotTo(HaveOccurred()) - _, err = wasmWriter.Write(wasmContent) - Expect(err).NotTo(HaveOccurred()) - - // Add README.md - readmeWriter, err := zipWriter.Create("README.md") - Expect(err).NotTo(HaveOccurred()) - _, err = readmeWriter.Write(readmeContent) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should load and validate a plugin package", func() { - pkg, err := LoadPackage(ndpPath) - Expect(err).NotTo(HaveOccurred()) - Expect(pkg).NotTo(BeNil()) - - // Check manifest was parsed - Expect(pkg.Manifest).NotTo(BeNil()) - Expect(pkg.Manifest.Name).To(Equal("test-plugin")) - Expect(pkg.Manifest.Author).To(Equal("Test Author")) - Expect(pkg.Manifest.Version).To(Equal("1.0.0")) - Expect(pkg.Manifest.Description).To(Equal("A test plugin")) - Expect(pkg.Manifest.Capabilities).To(HaveLen(1)) - Expect(pkg.Manifest.Capabilities[0]).To(Equal(schema.PluginManifestCapabilitiesElemMetadataAgent)) - - // Check WASM file was loaded - Expect(pkg.WasmBytes).NotTo(BeEmpty()) - - // Check docs were loaded - Expect(pkg.Docs).To(HaveKey("README.md")) - }) - - It("should extract a plugin package to a directory", func() { - targetDir := filepath.Join(tempDir, "extracted") - - err := ExtractPackage(ndpPath, targetDir) - Expect(err).NotTo(HaveOccurred()) - - // Check files were extracted - Expect(filepath.Join(targetDir, "manifest.json")).To(BeARegularFile()) - Expect(filepath.Join(targetDir, "plugin.wasm")).To(BeARegularFile()) - Expect(filepath.Join(targetDir, "README.md")).To(BeARegularFile()) - }) - - It("should fail to load an invalid package", func() { - // Create an invalid package (missing required files) - invalidPath := filepath.Join(tempDir, "invalid.ndp") - zipFile, err := os.Create(invalidPath) - Expect(err).NotTo(HaveOccurred()) - - zipWriter := zip.NewWriter(zipFile) - // Only add a README, missing manifest and wasm - readmeWriter, err := zipWriter.Create("README.md") - Expect(err).NotTo(HaveOccurred()) - _, err = readmeWriter.Write([]byte("Invalid package")) - Expect(err).NotTo(HaveOccurred()) - zipWriter.Close() - zipFile.Close() - - // Test loading fails - _, err = LoadPackage(invalidPath) - Expect(err).To(HaveOccurred()) - }) -}) diff --git a/plugins/plugin_lifecycle_manager.go b/plugins/plugin_lifecycle_manager.go deleted file mode 100644 index e00e7e5f3..000000000 --- a/plugins/plugin_lifecycle_manager.go +++ /dev/null @@ -1,95 +0,0 @@ -package plugins - -import ( - "context" - "maps" - "sync" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/api" -) - -// pluginLifecycleManager tracks which plugins have been initialized and manages their lifecycle -type pluginLifecycleManager struct { - plugins sync.Map // string -> bool - config map[string]map[string]string - metrics metrics.Metrics -} - -// newPluginLifecycleManager creates a new plugin lifecycle manager -func newPluginLifecycleManager(metrics metrics.Metrics) *pluginLifecycleManager { - config := maps.Clone(conf.Server.PluginConfig) - return &pluginLifecycleManager{ - config: config, - metrics: metrics, - } -} - -// isInitialized checks if a plugin has been initialized -func (m *pluginLifecycleManager) isInitialized(plugin *plugin) bool { - key := plugin.ID + consts.Zwsp + plugin.Manifest.Version - value, exists := m.plugins.Load(key) - return exists && value.(bool) -} - -// markInitialized marks a plugin as initialized -func (m *pluginLifecycleManager) markInitialized(plugin *plugin) { - key := plugin.ID + consts.Zwsp + plugin.Manifest.Version - m.plugins.Store(key, true) -} - -// clearInitialized removes the initialization state of a plugin -func (m *pluginLifecycleManager) clearInitialized(plugin *plugin) { - key := plugin.ID + consts.Zwsp + plugin.Manifest.Version - m.plugins.Delete(key) -} - -// callOnInit calls the OnInit method on a plugin that implements LifecycleManagement -func (m *pluginLifecycleManager) callOnInit(plugin *plugin) error { - ctx := context.Background() - log.Debug("Initializing plugin", "name", plugin.ID) - start := time.Now() - - // Create LifecycleManagement plugin instance - loader, err := api.NewLifecycleManagementPlugin(ctx, api.WazeroRuntime(plugin.Runtime), api.WazeroModuleConfig(plugin.ModConfig)) - if loader == nil || err != nil { - log.Error("Error creating LifecycleManagement plugin", "plugin", plugin.ID, err) - return err - } - - initPlugin, err := loader.Load(ctx, plugin.WasmPath) - if err != nil { - log.Error("Error loading LifecycleManagement plugin", "plugin", plugin.ID, "path", plugin.WasmPath, err) - return err - } - defer initPlugin.Close(ctx) - - // Prepare the request with plugin-specific configuration - req := &api.InitRequest{} - - // Add plugin configuration if available - if m.config != nil { - if pluginConfig, ok := m.config[plugin.ID]; ok && len(pluginConfig) > 0 { - req.Config = maps.Clone(pluginConfig) - log.Debug("Passing configuration to plugin", "plugin", plugin.ID, "configKeys", len(pluginConfig)) - } - } - - // Call OnInit - callStart := time.Now() - _, err = checkErr(initPlugin.OnInit(ctx, req)) - m.metrics.RecordPluginRequest(ctx, plugin.ID, "OnInit", err == nil, time.Since(callStart).Milliseconds()) - if err != nil { - log.Error("Error initializing plugin", "plugin", plugin.ID, "elapsed", time.Since(start), err) - return err - } - - // Mark the plugin as initialized - m.markInitialized(plugin) - log.Debug("Plugin initialized successfully", "plugin", plugin.ID, "elapsed", time.Since(start)) - return nil -} diff --git a/plugins/plugin_lifecycle_manager_test.go b/plugins/plugin_lifecycle_manager_test.go deleted file mode 100644 index 800630ce9..000000000 --- a/plugins/plugin_lifecycle_manager_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package plugins - -import ( - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -// Helper function to check if a plugin implements LifecycleManagement -func hasInitService(info *plugin) bool { - for _, c := range info.Capabilities { - if c == CapabilityLifecycleManagement { - return true - } - } - return false -} - -var _ = Describe("LifecycleManagement", func() { - Describe("Plugin Lifecycle Manager", func() { - var lifecycleManager *pluginLifecycleManager - - BeforeEach(func() { - lifecycleManager = newPluginLifecycleManager(metrics.NewNoopInstance()) - }) - - It("should track initialization state of plugins", func() { - // Create test plugins - plugin1 := &plugin{ - ID: "test-plugin", - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - plugin2 := &plugin{ - ID: "another-plugin", - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "0.5.0", - }, - } - - // Initially, no plugins should be initialized - Expect(lifecycleManager.isInitialized(plugin1)).To(BeFalse()) - Expect(lifecycleManager.isInitialized(plugin2)).To(BeFalse()) - - // Mark first plugin as initialized - lifecycleManager.markInitialized(plugin1) - - // Check state - Expect(lifecycleManager.isInitialized(plugin1)).To(BeTrue()) - Expect(lifecycleManager.isInitialized(plugin2)).To(BeFalse()) - - // Mark second plugin as initialized - lifecycleManager.markInitialized(plugin2) - - // Both should be initialized now - Expect(lifecycleManager.isInitialized(plugin1)).To(BeTrue()) - Expect(lifecycleManager.isInitialized(plugin2)).To(BeTrue()) - }) - - It("should handle plugins with same name but different versions", func() { - plugin1 := &plugin{ - ID: "test-plugin", - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - plugin2 := &plugin{ - ID: "test-plugin", // Same name - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "2.0.0", // Different version - }, - } - - // Mark v1 as initialized - lifecycleManager.markInitialized(plugin1) - - // v1 should be initialized but not v2 - Expect(lifecycleManager.isInitialized(plugin1)).To(BeTrue()) - Expect(lifecycleManager.isInitialized(plugin2)).To(BeFalse()) - - // Mark v2 as initialized - lifecycleManager.markInitialized(plugin2) - - // Both versions should be initialized now - Expect(lifecycleManager.isInitialized(plugin1)).To(BeTrue()) - Expect(lifecycleManager.isInitialized(plugin2)).To(BeTrue()) - - // Verify the keys used for tracking - key1 := plugin1.ID + consts.Zwsp + plugin1.Manifest.Version - key2 := plugin1.ID + consts.Zwsp + plugin2.Manifest.Version - _, exists1 := lifecycleManager.plugins.Load(key1) - _, exists2 := lifecycleManager.plugins.Load(key2) - Expect(exists1).To(BeTrue()) - Expect(exists2).To(BeTrue()) - Expect(key1).NotTo(Equal(key2)) - }) - - It("should only consider plugins that implement LifecycleManagement", func() { - // Plugin that implements LifecycleManagement - initPlugin := &plugin{ - ID: "init-plugin", - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - // Plugin that doesn't implement LifecycleManagement - regularPlugin := &plugin{ - ID: "regular-plugin", - Capabilities: []string{"MetadataAgent"}, - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - // Check if plugins can be initialized - Expect(hasInitService(initPlugin)).To(BeTrue()) - Expect(hasInitService(regularPlugin)).To(BeFalse()) - }) - - It("should properly construct the plugin key", func() { - plugin := &plugin{ - ID: "test-plugin", - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - expectedKey := "test-plugin" + consts.Zwsp + "1.0.0" - actualKey := plugin.ID + consts.Zwsp + plugin.Manifest.Version - - Expect(actualKey).To(Equal(expectedKey)) - }) - - It("should clear initialization state when requested", func() { - plugin := &plugin{ - ID: "test-plugin", - Capabilities: []string{CapabilityLifecycleManagement}, - Manifest: &schema.PluginManifest{ - Version: "1.0.0", - }, - } - - // Initially not initialized - Expect(lifecycleManager.isInitialized(plugin)).To(BeFalse()) - - // Mark as initialized - lifecycleManager.markInitialized(plugin) - Expect(lifecycleManager.isInitialized(plugin)).To(BeTrue()) - - // Clear initialization state - lifecycleManager.clearInitialized(plugin) - Expect(lifecycleManager.isInitialized(plugin)).To(BeFalse()) - }) - }) -}) diff --git a/plugins/plugins_suite_test.go b/plugins/plugins_suite_test.go deleted file mode 100644 index 153426317..000000000 --- a/plugins/plugins_suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package plugins - -import ( - "os/exec" - "testing" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -const testDataDir = "plugins/testdata" - -func TestPlugins(t *testing.T) { - tests.Init(t, false) - buildTestPlugins(t, testDataDir) - log.SetLevel(log.LevelFatal) - RegisterFailHandler(Fail) - RunSpecs(t, "Plugins Suite") -} - -func buildTestPlugins(t *testing.T, path string) { - t.Helper() - t.Logf("[BeforeSuite] Current working directory: %s", path) - cmd := exec.Command("make", "-C", path) - out, err := cmd.CombinedOutput() - t.Logf("[BeforeSuite] Make output: %s", string(out)) - if err != nil { - t.Fatalf("Failed to build test plugins: %v", err) - } -} diff --git a/plugins/runtime.go b/plugins/runtime.go deleted file mode 100644 index ee298e63d..000000000 --- a/plugins/runtime.go +++ /dev/null @@ -1,626 +0,0 @@ -package plugins - -import ( - "context" - "crypto/md5" - "fmt" - "io/fs" - "maps" - "os" - "path/filepath" - "sort" - "sync" - "sync/atomic" - "time" - - "github.com/dustin/go-humanize" - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/artwork" - "github.com/navidrome/navidrome/plugins/host/cache" - "github.com/navidrome/navidrome/plugins/host/config" - "github.com/navidrome/navidrome/plugins/host/http" - "github.com/navidrome/navidrome/plugins/host/scheduler" - "github.com/navidrome/navidrome/plugins/host/subsonicapi" - "github.com/navidrome/navidrome/plugins/host/websocket" - "github.com/navidrome/navidrome/plugins/schema" - "github.com/tetratelabs/wazero" - wazeroapi "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" -) - -const maxParallelCompilations = 2 // Limit to 2 concurrent compilations - -var ( - compileSemaphore = make(chan struct{}, maxParallelCompilations) - compilationCache wazero.CompilationCache - cacheOnce sync.Once - runtimePool sync.Map // map[string]*cachingRuntime -) - -// createRuntime returns a function that creates a new wazero runtime and instantiates the required host functions -// based on the given plugin permissions -func (m *managerImpl) createRuntime(pluginID string, permissions schema.PluginManifestPermissions) api.WazeroNewRuntime { - return func(ctx context.Context) (wazero.Runtime, error) { - // Check if runtime already exists - if rt, ok := runtimePool.Load(pluginID); ok { - log.Trace(ctx, "Using existing runtime", "plugin", pluginID, "runtime", fmt.Sprintf("%p", rt)) - // Return a new wrapper for each call, so each instance gets its own module capture - return newScopedRuntime(rt.(wazero.Runtime)), nil - } - - // Create new runtime with all the setup - cachingRT, err := m.createCachingRuntime(ctx, pluginID, permissions) - if err != nil { - return nil, err - } - - // Use LoadOrStore to atomically check and store, preventing race conditions - if existing, loaded := runtimePool.LoadOrStore(pluginID, cachingRT); loaded { - // Another goroutine created the runtime first, close ours and return the existing one - log.Trace(ctx, "Race condition detected, using existing runtime", "plugin", pluginID, "runtime", fmt.Sprintf("%p", existing)) - _ = cachingRT.Close(ctx) - return newScopedRuntime(existing.(wazero.Runtime)), nil - } - - log.Trace(ctx, "Created new runtime", "plugin", pluginID, "runtime", fmt.Sprintf("%p", cachingRT)) - return newScopedRuntime(cachingRT), nil - } -} - -// createCachingRuntime handles the complex logic of setting up a new cachingRuntime -func (m *managerImpl) createCachingRuntime(ctx context.Context, pluginID string, permissions schema.PluginManifestPermissions) (*cachingRuntime, error) { - // Get compilation cache - compCache, err := getCompilationCache() - if err != nil { - return nil, fmt.Errorf("failed to get compilation cache: %w", err) - } - - // Create the runtime - runtimeConfig := wazero.NewRuntimeConfig().WithCompilationCache(compCache) - r := wazero.NewRuntimeWithConfig(ctx, runtimeConfig) - if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil { - return nil, err - } - - // Setup host services - if err := m.setupHostServices(ctx, r, pluginID, permissions); err != nil { - _ = r.Close(ctx) - return nil, err - } - - return newCachingRuntime(r, pluginID), nil -} - -// setupHostServices configures all the permitted host services for a plugin -func (m *managerImpl) setupHostServices(ctx context.Context, r wazero.Runtime, pluginID string, permissions schema.PluginManifestPermissions) error { - // Define all available host services - type hostService struct { - name string - isPermitted bool - loadFunc func() (map[string]wazeroapi.FunctionDefinition, error) - } - - // List of all available host services with their permissions and loading functions - availableServices := []hostService{ - {"config", permissions.Config != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - return loadHostLibrary[config.ConfigService](ctx, config.Instantiate, &configServiceImpl{pluginID: pluginID}) - }}, - {"scheduler", permissions.Scheduler != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - return loadHostLibrary[scheduler.SchedulerService](ctx, scheduler.Instantiate, m.schedulerService.HostFunctions(pluginID)) - }}, - {"cache", permissions.Cache != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - return loadHostLibrary[cache.CacheService](ctx, cache.Instantiate, newCacheService(pluginID)) - }}, - {"artwork", permissions.Artwork != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - return loadHostLibrary[artwork.ArtworkService](ctx, artwork.Instantiate, &artworkServiceImpl{}) - }}, - {"http", permissions.Http != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - httpPerms, err := parseHTTPPermissions(permissions.Http) - if err != nil { - return nil, fmt.Errorf("invalid http permissions for plugin %s: %w", pluginID, err) - } - return loadHostLibrary[http.HttpService](ctx, http.Instantiate, &httpServiceImpl{ - pluginID: pluginID, - permissions: httpPerms, - }) - }}, - {"websocket", permissions.Websocket != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - wsPerms, err := parseWebSocketPermissions(permissions.Websocket) - if err != nil { - return nil, fmt.Errorf("invalid websocket permissions for plugin %s: %w", pluginID, err) - } - return loadHostLibrary[websocket.WebSocketService](ctx, websocket.Instantiate, m.websocketService.HostFunctions(pluginID, wsPerms)) - }}, - {"subsonicapi", permissions.Subsonicapi != nil, func() (map[string]wazeroapi.FunctionDefinition, error) { - if router := m.subsonicRouter.Load(); router != nil { - service := newSubsonicAPIService(pluginID, m.subsonicRouter.Load(), m.ds, permissions.Subsonicapi) - return loadHostLibrary[subsonicapi.SubsonicAPIService](ctx, subsonicapi.Instantiate, service) - } - log.Error(ctx, "SubsonicAPI service requested but router not available", "plugin", pluginID) - return nil, fmt.Errorf("SubsonicAPI router not available for plugin %s", pluginID) - }}, - } - - // Load only permitted services - var grantedPermissions []string - var libraries []map[string]wazeroapi.FunctionDefinition - for _, service := range availableServices { - if service.isPermitted { - lib, err := service.loadFunc() - if err != nil { - return fmt.Errorf("error loading %s lib: %w", service.name, err) - } - libraries = append(libraries, lib) - grantedPermissions = append(grantedPermissions, service.name) - } - } - log.Trace(ctx, "Granting permissions for plugin", "plugin", pluginID, "permissions", grantedPermissions) - - // Combine the permitted libraries - return combineLibraries(ctx, r, libraries...) -} - -// purgeCacheBySize removes the oldest files in dir until its total size is -// lower than or equal to maxSize. maxSize should be a human-readable string -// like "10MB" or "200K". If parsing fails or maxSize is "0", the function is -// a no-op. -func purgeCacheBySize(dir, maxSize string) { - sizeLimit, err := humanize.ParseBytes(maxSize) - if err != nil || sizeLimit == 0 { - return - } - - type fileInfo struct { - path string - size uint64 - mod int64 - } - - var files []fileInfo - var total uint64 - - walk := func(path string, d fs.DirEntry, err error) error { - if err != nil { - log.Trace("Failed to access plugin cache entry", "path", path, err) - return nil //nolint:nilerr - } - if d.IsDir() { - return nil - } - info, err := d.Info() - if err != nil { - log.Trace("Failed to get file info for plugin cache entry", "path", path, err) - return nil //nolint:nilerr - } - files = append(files, fileInfo{ - path: path, - size: uint64(info.Size()), - mod: info.ModTime().UnixMilli(), - }) - total += uint64(info.Size()) - return nil - } - - if err := filepath.WalkDir(dir, walk); err != nil { - if !os.IsNotExist(err) { - log.Warn("Failed to traverse plugin cache directory", "path", dir, err) - } - return - } - - log.Trace("Current plugin cache size", "path", dir, "size", humanize.Bytes(total), "sizeLimit", humanize.Bytes(sizeLimit)) - if total <= sizeLimit { - return - } - - log.Debug("Purging plugin cache", "path", dir, "sizeLimit", humanize.Bytes(sizeLimit), "currentSize", humanize.Bytes(total)) - sort.Slice(files, func(i, j int) bool { return files[i].mod < files[j].mod }) - for _, f := range files { - if total <= sizeLimit { - break - } - if err := os.Remove(f.path); err != nil { - log.Warn("Failed to remove plugin cache entry", "path", f.path, "size", humanize.Bytes(f.size), err) - continue - } - total -= f.size - log.Debug("Removed plugin cache entry", "path", f.path, "size", humanize.Bytes(f.size), "time", time.UnixMilli(f.mod), "remainingSize", humanize.Bytes(total)) - - // Remove empty parent directories - dirPath := filepath.Dir(f.path) - for dirPath != dir { - if err := os.Remove(dirPath); err != nil { - break - } - dirPath = filepath.Dir(dirPath) - } - } -} - -// getCompilationCache returns the global compilation cache, creating it if necessary -func getCompilationCache() (wazero.CompilationCache, error) { - var err error - cacheOnce.Do(func() { - cacheDir := filepath.Join(conf.Server.CacheFolder, "plugins") - purgeCacheBySize(cacheDir, conf.Server.Plugins.CacheSize) - compilationCache, err = wazero.NewCompilationCacheWithDir(cacheDir) - }) - return compilationCache, err -} - -// newWazeroModuleConfig creates the correct ModuleConfig for plugins -func newWazeroModuleConfig() wazero.ModuleConfig { - return wazero.NewModuleConfig().WithStartFunctions("_initialize").WithStderr(log.Writer()) -} - -// pluginCompilationTimeout returns the timeout for plugin compilation -func pluginCompilationTimeout() time.Duration { - if conf.Server.DevPluginCompilationTimeout > 0 { - return conf.Server.DevPluginCompilationTimeout - } - return time.Minute -} - -// precompilePlugin compiles the WASM module in the background and updates the pluginState. -func precompilePlugin(p *plugin) { - compileSemaphore <- struct{}{} - defer func() { <-compileSemaphore }() - ctx := context.Background() - r, err := p.Runtime(ctx) - if err != nil { - p.compilationErr = fmt.Errorf("failed to create runtime for plugin %s: %w", p.ID, err) - close(p.compilationReady) - return - } - - b, err := os.ReadFile(p.WasmPath) - if err != nil { - p.compilationErr = fmt.Errorf("failed to read wasm file: %w", err) - close(p.compilationReady) - return - } - - // We know r is always a *scopedRuntime from createRuntime - scopedRT := r.(*scopedRuntime) - cachingRT := scopedRT.GetCachingRuntime() - if cachingRT == nil { - p.compilationErr = fmt.Errorf("failed to get cachingRuntime for plugin %s", p.ID) - close(p.compilationReady) - return - } - - _, err = cachingRT.CompileModule(ctx, b) - if err != nil { - p.compilationErr = fmt.Errorf("failed to compile WASM for plugin %s: %w", p.ID, err) - log.Warn("Plugin compilation failed", "name", p.ID, "path", p.WasmPath, "err", err) - } else { - p.compilationErr = nil - log.Debug("Plugin compilation completed", "name", p.ID, "path", p.WasmPath) - } - close(p.compilationReady) -} - -// loadHostLibrary loads the given host library and returns its exported functions -func loadHostLibrary[S any]( - ctx context.Context, - instantiateFn func(context.Context, wazero.Runtime, S) error, - service S, -) (map[string]wazeroapi.FunctionDefinition, error) { - r := wazero.NewRuntime(ctx) - if err := instantiateFn(ctx, r, service); err != nil { - return nil, err - } - m := r.Module("env") - return m.ExportedFunctionDefinitions(), nil -} - -// combineLibraries combines the given host libraries into a single "env" module -func combineLibraries(ctx context.Context, r wazero.Runtime, libs ...map[string]wazeroapi.FunctionDefinition) error { - // Merge the libraries - hostLib := map[string]wazeroapi.FunctionDefinition{} - for _, lib := range libs { - maps.Copy(hostLib, lib) - } - - // Create the combined host module - envBuilder := r.NewHostModuleBuilder("env") - for name, fd := range hostLib { - fn, ok := fd.GoFunction().(wazeroapi.GoModuleFunction) - if !ok { - return fmt.Errorf("invalid function definition: %s", fd.DebugName()) - } - envBuilder.NewFunctionBuilder(). - WithGoModuleFunction(fn, fd.ParamTypes(), fd.ResultTypes()). - WithParameterNames(fd.ParamNames()...).Export(name) - } - - // Instantiate the combined host module - if _, err := envBuilder.Instantiate(ctx); err != nil { - return err - } - return nil -} - -const ( - // WASM Instance pool configuration - // defaultPoolSize is the maximum number of instances per plugin that are kept in the pool for reuse - defaultPoolSize = 8 - // defaultInstanceTTL is the time after which an instance is considered stale and can be evicted - defaultInstanceTTL = time.Minute - // defaultMaxConcurrentInstances is the hard limit on total instances that can exist simultaneously - defaultMaxConcurrentInstances = 10 - // defaultGetTimeout is the maximum time to wait when getting an instance if at the concurrent limit - defaultGetTimeout = 5 * time.Second - - // Compiled module cache configuration - // defaultCompiledModuleTTL is the time after which a compiled module is evicted from the cache - defaultCompiledModuleTTL = 5 * time.Minute -) - -// cachedCompiledModule encapsulates a compiled WebAssembly module with TTL management -type cachedCompiledModule struct { - module wazero.CompiledModule - hash [16]byte - lastAccess time.Time - timer *time.Timer - mu sync.Mutex - pluginID string // for logging purposes -} - -// newCachedCompiledModule creates a new cached compiled module with TTL management -func newCachedCompiledModule(module wazero.CompiledModule, wasmBytes []byte, pluginID string) *cachedCompiledModule { - c := &cachedCompiledModule{ - module: module, - hash: md5.Sum(wasmBytes), - lastAccess: time.Now(), - pluginID: pluginID, - } - - // Set up the TTL timer - c.timer = time.AfterFunc(defaultCompiledModuleTTL, c.evict) - - return c -} - -// get returns the cached module if the hash matches, nil otherwise -// Also resets the TTL timer on successful access -func (c *cachedCompiledModule) get(wasmHash [16]byte) wazero.CompiledModule { - c.mu.Lock() // Use write lock because we modify state in resetTimer - defer c.mu.Unlock() - - if c.module != nil && c.hash == wasmHash { - // Reset TTL timer on access - c.resetTimer() - return c.module - } - - return nil -} - -// resetTimer resets the TTL timer (must be called with lock held) -func (c *cachedCompiledModule) resetTimer() { - c.lastAccess = time.Now() - - if c.timer != nil { - c.timer.Stop() - c.timer = time.AfterFunc(defaultCompiledModuleTTL, c.evict) - } -} - -// evict removes the cached module and cleans up resources -func (c *cachedCompiledModule) evict() { - c.mu.Lock() - defer c.mu.Unlock() - - if c.module != nil { - log.Trace("cachedCompiledModule: evicting due to TTL expiry", "plugin", c.pluginID, "ttl", defaultCompiledModuleTTL) - c.module.Close(context.Background()) - c.module = nil - c.hash = [16]byte{} - c.lastAccess = time.Time{} - } - - if c.timer != nil { - c.timer.Stop() - c.timer = nil - } -} - -// close cleans up the cached module and stops the timer -func (c *cachedCompiledModule) close(ctx context.Context) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.timer != nil { - c.timer.Stop() - c.timer = nil - } - - if c.module != nil { - c.module.Close(ctx) - c.module = nil - } -} - -// pooledModule wraps a wazero Module and returns it to the pool when closed. -type pooledModule struct { - wazeroapi.Module - pool *wasmInstancePool[wazeroapi.Module] - closed bool -} - -func (m *pooledModule) Close(ctx context.Context) error { - if !m.closed { - m.closed = true - m.pool.Put(ctx, m.Module) - } - return nil -} - -func (m *pooledModule) CloseWithExitCode(ctx context.Context, exitCode uint32) error { - return m.Close(ctx) -} - -func (m *pooledModule) IsClosed() bool { - return m.closed -} - -// newScopedRuntime creates a new scopedRuntime that wraps the given runtime -func newScopedRuntime(runtime wazero.Runtime) *scopedRuntime { - return &scopedRuntime{Runtime: runtime} -} - -// scopedRuntime wraps a cachingRuntime and captures a specific module -// so that Close() only affects that module, not the entire shared runtime -type scopedRuntime struct { - wazero.Runtime - capturedModule wazeroapi.Module -} - -func (w *scopedRuntime) InstantiateModule(ctx context.Context, code wazero.CompiledModule, config wazero.ModuleConfig) (wazeroapi.Module, error) { - module, err := w.Runtime.InstantiateModule(ctx, code, config) - if err != nil { - return nil, err - } - // Capture the module for later cleanup - w.capturedModule = module - log.Trace(ctx, "scopedRuntime: captured module", "moduleID", getInstanceID(module)) - return module, nil -} - -func (w *scopedRuntime) Close(ctx context.Context) error { - // Close only the captured module, not the entire runtime - if w.capturedModule != nil { - log.Trace(ctx, "scopedRuntime: closing captured module", "moduleID", getInstanceID(w.capturedModule)) - return w.capturedModule.Close(ctx) - } - log.Trace(ctx, "scopedRuntime: no captured module to close") - return nil -} - -func (w *scopedRuntime) CloseWithExitCode(ctx context.Context, exitCode uint32) error { - return w.Close(ctx) -} - -// GetCachingRuntime returns the underlying cachingRuntime for internal use -func (w *scopedRuntime) GetCachingRuntime() *cachingRuntime { - if cr, ok := w.Runtime.(*cachingRuntime); ok { - return cr - } - return nil -} - -// cachingRuntime wraps wazero.Runtime and pools module instances per plugin, -// while also caching the compiled module in memory. -type cachingRuntime struct { - wazero.Runtime - - // pluginID is required to differentiate between different plugins that use the same file to initialize their - // runtime. The runtime will serve as a singleton for all instances of a given plugin. - pluginID string - - // cachedModule manages the compiled module cache with TTL - cachedModule atomic.Pointer[cachedCompiledModule] - - // pool manages reusable module instances - pool *wasmInstancePool[wazeroapi.Module] - - // poolInitOnce ensures the pool is initialized only once - poolInitOnce sync.Once - - // compilationMu ensures only one compilation happens at a time per runtime - compilationMu sync.Mutex -} - -func newCachingRuntime(runtime wazero.Runtime, pluginID string) *cachingRuntime { - return &cachingRuntime{ - Runtime: runtime, - pluginID: pluginID, - } -} - -func (r *cachingRuntime) initPool(code wazero.CompiledModule, config wazero.ModuleConfig) { - r.poolInitOnce.Do(func() { - r.pool = newWasmInstancePool[wazeroapi.Module](r.pluginID, defaultPoolSize, defaultMaxConcurrentInstances, defaultGetTimeout, defaultInstanceTTL, func(ctx context.Context) (wazeroapi.Module, error) { - log.Trace(ctx, "cachingRuntime: creating new module instance", "plugin", r.pluginID) - return r.Runtime.InstantiateModule(ctx, code, config) - }) - }) -} - -func (r *cachingRuntime) InstantiateModule(ctx context.Context, code wazero.CompiledModule, config wazero.ModuleConfig) (wazeroapi.Module, error) { - r.initPool(code, config) - mod, err := r.pool.Get(ctx) - if err != nil { - return nil, err - } - wrapped := &pooledModule{Module: mod, pool: r.pool} - log.Trace(ctx, "cachingRuntime: created wrapper for module", "plugin", r.pluginID, "underlyingModuleID", fmt.Sprintf("%p", mod), "wrapperID", fmt.Sprintf("%p", wrapped)) - return wrapped, nil -} - -func (r *cachingRuntime) Close(ctx context.Context) error { - log.Trace(ctx, "cachingRuntime: closing runtime", "plugin", r.pluginID) - - // Clean up compiled module cache - if cached := r.cachedModule.Swap(nil); cached != nil { - cached.close(ctx) - } - - // Close the instance pool - if r.pool != nil { - r.pool.Close(ctx) - } - // Close the underlying runtime - return r.Runtime.Close(ctx) -} - -// setCachedModule stores a newly compiled module in the cache with TTL management -func (r *cachingRuntime) setCachedModule(module wazero.CompiledModule, wasmBytes []byte) { - newCached := newCachedCompiledModule(module, wasmBytes, r.pluginID) - - // Replace old cached module and clean it up - if old := r.cachedModule.Swap(newCached); old != nil { - old.close(context.Background()) - } -} - -// CompileModule checks if the provided bytes match our cached hash and returns -// the cached compiled module if so, avoiding both file read and compilation. -func (r *cachingRuntime) CompileModule(ctx context.Context, wasmBytes []byte) (wazero.CompiledModule, error) { - incomingHash := md5.Sum(wasmBytes) - - // Try to get from cache first (without lock for performance) - if cached := r.cachedModule.Load(); cached != nil { - if module := cached.get(incomingHash); module != nil { - log.Trace(ctx, "cachingRuntime: using cached compiled module", "plugin", r.pluginID) - return module, nil - } - } - - // Synchronize compilation to prevent concurrent compilation issues - r.compilationMu.Lock() - defer r.compilationMu.Unlock() - - // Double-check cache after acquiring lock (another goroutine might have compiled it) - if cached := r.cachedModule.Load(); cached != nil { - if module := cached.get(incomingHash); module != nil { - log.Trace(ctx, "cachingRuntime: using cached compiled module (after lock)", "plugin", r.pluginID) - return module, nil - } - } - - // Fall back to normal compilation for different bytes - log.Trace(ctx, "cachingRuntime: hash doesn't match cache, compiling normally", "plugin", r.pluginID) - module, err := r.Runtime.CompileModule(ctx, wasmBytes) - if err != nil { - return nil, err - } - - // Cache the newly compiled module - r.setCachedModule(module, wasmBytes) - - return module, nil -} diff --git a/plugins/runtime_test.go b/plugins/runtime_test.go deleted file mode 100644 index 05efe1d1d..000000000 --- a/plugins/runtime_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/core/metrics" - "github.com/navidrome/navidrome/plugins/schema" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/tetratelabs/wazero" -) - -var _ = Describe("Runtime", func() { - Describe("pluginCompilationTimeout", func() { - It("should use DevPluginCompilationTimeout config for plugin compilation timeout", func() { - originalTimeout := conf.Server.DevPluginCompilationTimeout - DeferCleanup(func() { - conf.Server.DevPluginCompilationTimeout = originalTimeout - }) - - conf.Server.DevPluginCompilationTimeout = 123 * time.Second - Expect(pluginCompilationTimeout()).To(Equal(123 * time.Second)) - - conf.Server.DevPluginCompilationTimeout = 0 - Expect(pluginCompilationTimeout()).To(Equal(time.Minute)) - }) - }) -}) - -var _ = Describe("CachingRuntime", func() { - var ( - ctx context.Context - mgr *managerImpl - plugin *wasmScrobblerPlugin - ) - - BeforeEach(func() { - ctx = GinkgoT().Context() - mgr = createManager(nil, metrics.NewNoopInstance()) - // Add permissions for the test plugin using typed struct - permissions := schema.PluginManifestPermissions{ - Http: &schema.PluginManifestPermissionsHttp{ - Reason: "For testing HTTP functionality", - AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{ - "*": {schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard}, - }, - AllowLocalNetwork: false, - }, - Config: &schema.PluginManifestPermissionsConfig{ - Reason: "For testing config functionality", - }, - } - rtFunc := mgr.createRuntime("fake_scrobbler", permissions) - plugin = newWasmScrobblerPlugin( - filepath.Join(testDataDir, "fake_scrobbler", "plugin.wasm"), - "fake_scrobbler", - mgr, - rtFunc, - wazero.NewModuleConfig().WithStartFunctions("_initialize"), - ).(*wasmScrobblerPlugin) - // runtime will be created on first plugin load - }) - - It("reuses module instances across calls", func() { - // First call to create the runtime and pool - _, done, err := plugin.getInstance(ctx, "first") - Expect(err).ToNot(HaveOccurred()) - done() - - val, ok := runtimePool.Load("fake_scrobbler") - Expect(ok).To(BeTrue()) - cachingRT := val.(*cachingRuntime) - - // Verify the pool exists and is initialized - Expect(cachingRT.pool).ToNot(BeNil()) - - // Test that multiple calls work without error (indicating pool reuse) - for i := 0; i < 5; i++ { - inst, done, err := plugin.getInstance(ctx, fmt.Sprintf("call_%d", i)) - Expect(err).ToNot(HaveOccurred()) - Expect(inst).ToNot(BeNil()) - done() - } - - // Test concurrent access to verify pool handles concurrency - const numGoroutines = 3 - errChan := make(chan error, numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(id int) { - inst, done, err := plugin.getInstance(ctx, fmt.Sprintf("concurrent_%d", id)) - if err != nil { - errChan <- err - return - } - defer done() - - // Verify we got a valid instance - if inst == nil { - errChan <- fmt.Errorf("got nil instance") - return - } - errChan <- nil - }(i) - } - - // Check all goroutines succeeded - for i := 0; i < numGoroutines; i++ { - err := <-errChan - Expect(err).To(BeNil()) - } - }) -}) - -var _ = Describe("purgeCacheBySize", func() { - var tmpDir string - - BeforeEach(func() { - var err error - tmpDir, err = os.MkdirTemp("", "cache_test") - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(os.RemoveAll, tmpDir) - }) - - It("removes oldest entries when above the size limit", func() { - oldDir := filepath.Join(tmpDir, "d1") - newDir := filepath.Join(tmpDir, "d2") - Expect(os.Mkdir(oldDir, 0700)).To(Succeed()) - Expect(os.Mkdir(newDir, 0700)).To(Succeed()) - - oldFile := filepath.Join(oldDir, "old") - newFile := filepath.Join(newDir, "new") - Expect(os.WriteFile(oldFile, []byte("xx"), 0600)).To(Succeed()) - Expect(os.WriteFile(newFile, []byte("xx"), 0600)).To(Succeed()) - - oldTime := time.Now().Add(-2 * time.Hour) - Expect(os.Chtimes(oldFile, oldTime, oldTime)).To(Succeed()) - - purgeCacheBySize(tmpDir, "3") - - _, err := os.Stat(oldFile) - Expect(os.IsNotExist(err)).To(BeTrue()) - _, err = os.Stat(oldDir) - Expect(os.IsNotExist(err)).To(BeTrue()) - - _, err = os.Stat(newFile) - Expect(err).ToNot(HaveOccurred()) - }) - - It("does nothing when below the size limit", func() { - dir1 := filepath.Join(tmpDir, "a") - dir2 := filepath.Join(tmpDir, "b") - Expect(os.Mkdir(dir1, 0700)).To(Succeed()) - Expect(os.Mkdir(dir2, 0700)).To(Succeed()) - - file1 := filepath.Join(dir1, "f1") - file2 := filepath.Join(dir2, "f2") - Expect(os.WriteFile(file1, []byte("x"), 0600)).To(Succeed()) - Expect(os.WriteFile(file2, []byte("x"), 0600)).To(Succeed()) - - purgeCacheBySize(tmpDir, "10MB") - - _, err := os.Stat(file1) - Expect(err).ToNot(HaveOccurred()) - _, err = os.Stat(file2) - Expect(err).ToNot(HaveOccurred()) - }) -}) diff --git a/plugins/schema/manifest.schema.json b/plugins/schema/manifest.schema.json deleted file mode 100644 index 0c323126b..000000000 --- a/plugins/schema/manifest.schema.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "$id": "navidrome://plugins/manifest", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Navidrome Plugin Manifest", - "description": "Schema for Navidrome Plugin manifest.json files", - "type": "object", - "required": [ - "name", - "author", - "version", - "description", - "website", - "capabilities", - "permissions" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the plugin" - }, - "author": { - "type": "string", - "description": "Author or organization that created the plugin" - }, - "version": { - "type": "string", - "description": "Plugin version using semantic versioning format" - }, - "description": { - "type": "string", - "description": "A brief description of the plugin's functionality" - }, - "website": { - "type": "string", - "format": "uri", - "description": "Website URL for the plugin or its documentation" - }, - "capabilities": { - "type": "array", - "description": "List of capabilities implemented by this plugin", - "minItems": 1, - "items": { - "type": "string", - "enum": [ - "MetadataAgent", - "Scrobbler", - "SchedulerCallback", - "LifecycleManagement", - "WebSocketCallback" - ] - } - }, - "permissions": { - "type": "object", - "description": "Host services the plugin is allowed to access", - "additionalProperties": true, - "properties": { - "http": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "HTTP service permissions", - "required": ["allowedUrls"], - "properties": { - "allowedUrls": { - "type": "object", - "description": "Map of URL patterns (e.g., 'https://api.example.com/*') to allowed HTTP methods. Redirect destinations must also be included.", - "additionalProperties": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "GET", - "POST", - "PUT", - "DELETE", - "PATCH", - "HEAD", - "OPTIONS", - "*" - ] - }, - "minItems": 1, - "uniqueItems": true - }, - "minProperties": 1 - }, - "allowLocalNetwork": { - "type": "boolean", - "description": "Whether to allow requests to local/private network addresses", - "default": false - } - } - } - ] - }, - "config": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "Configuration service permissions" - } - ] - }, - "scheduler": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "Scheduler service permissions" - } - ] - }, - "websocket": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "WebSocket service permissions", - "required": ["allowedUrls"], - "properties": { - "allowedUrls": { - "type": "array", - "description": "List of WebSocket URL patterns that the plugin is allowed to connect to", - "items": { - "type": "string", - "pattern": "^wss?://.*$" - }, - "minItems": 1, - "uniqueItems": true - }, - "allowLocalNetwork": { - "type": "boolean", - "description": "Whether to allow connections to local/private network addresses", - "default": false - } - } - } - ] - }, - "cache": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "Cache service permissions" - } - ] - }, - "artwork": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "Artwork service permissions" - } - ] - }, - "subsonicapi": { - "allOf": [ - { "$ref": "#/$defs/basePermission" }, - { - "type": "object", - "description": "SubsonicAPI service permissions", - "properties": { - "allowedUsernames": { - "type": "array", - "description": "List of usernames the plugin can pass as u. Any user if empty", - "items": { "type": "string" } - }, - "allowAdmins": { - "type": "boolean", - "description": "If false, reject calls where the u is an admin", - "default": false - } - } - } - ] - } - } - } - }, - "$defs": { - "basePermission": { - "type": "object", - "required": ["reason"], - "properties": { - "reason": { - "type": "string", - "minLength": 1, - "description": "Explanation of why this permission is needed" - } - }, - "additionalProperties": false - } - } -} diff --git a/plugins/schema/manifest_gen.go b/plugins/schema/manifest_gen.go deleted file mode 100644 index 97e07a077..000000000 --- a/plugins/schema/manifest_gen.go +++ /dev/null @@ -1,426 +0,0 @@ -// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT. - -package schema - -import "encoding/json" -import "fmt" -import "reflect" - -type BasePermission struct { - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *BasePermission) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in BasePermission: required") - } - type Plain BasePermission - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = BasePermission(plain) - return nil -} - -// Schema for Navidrome Plugin manifest.json files -type PluginManifest struct { - // Author or organization that created the plugin - Author string `json:"author" yaml:"author" mapstructure:"author"` - - // List of capabilities implemented by this plugin - Capabilities []PluginManifestCapabilitiesElem `json:"capabilities" yaml:"capabilities" mapstructure:"capabilities"` - - // A brief description of the plugin's functionality - Description string `json:"description" yaml:"description" mapstructure:"description"` - - // Name of the plugin - Name string `json:"name" yaml:"name" mapstructure:"name"` - - // Host services the plugin is allowed to access - Permissions PluginManifestPermissions `json:"permissions" yaml:"permissions" mapstructure:"permissions"` - - // Plugin version using semantic versioning format - Version string `json:"version" yaml:"version" mapstructure:"version"` - - // Website URL for the plugin or its documentation - Website string `json:"website" yaml:"website" mapstructure:"website"` -} - -type PluginManifestCapabilitiesElem string - -const PluginManifestCapabilitiesElemLifecycleManagement PluginManifestCapabilitiesElem = "LifecycleManagement" -const PluginManifestCapabilitiesElemMetadataAgent PluginManifestCapabilitiesElem = "MetadataAgent" -const PluginManifestCapabilitiesElemSchedulerCallback PluginManifestCapabilitiesElem = "SchedulerCallback" -const PluginManifestCapabilitiesElemScrobbler PluginManifestCapabilitiesElem = "Scrobbler" -const PluginManifestCapabilitiesElemWebSocketCallback PluginManifestCapabilitiesElem = "WebSocketCallback" - -var enumValues_PluginManifestCapabilitiesElem = []interface{}{ - "MetadataAgent", - "Scrobbler", - "SchedulerCallback", - "LifecycleManagement", - "WebSocketCallback", -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestCapabilitiesElem) UnmarshalJSON(value []byte) error { - var v string - if err := json.Unmarshal(value, &v); err != nil { - return err - } - var ok bool - for _, expected := range enumValues_PluginManifestCapabilitiesElem { - if reflect.DeepEqual(v, expected) { - ok = true - break - } - } - if !ok { - return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_PluginManifestCapabilitiesElem, v) - } - *j = PluginManifestCapabilitiesElem(v) - return nil -} - -// Host services the plugin is allowed to access -type PluginManifestPermissions struct { - // Artwork corresponds to the JSON schema field "artwork". - Artwork *PluginManifestPermissionsArtwork `json:"artwork,omitempty" yaml:"artwork,omitempty" mapstructure:"artwork,omitempty"` - - // Cache corresponds to the JSON schema field "cache". - Cache *PluginManifestPermissionsCache `json:"cache,omitempty" yaml:"cache,omitempty" mapstructure:"cache,omitempty"` - - // Config corresponds to the JSON schema field "config". - Config *PluginManifestPermissionsConfig `json:"config,omitempty" yaml:"config,omitempty" mapstructure:"config,omitempty"` - - // Http corresponds to the JSON schema field "http". - Http *PluginManifestPermissionsHttp `json:"http,omitempty" yaml:"http,omitempty" mapstructure:"http,omitempty"` - - // Scheduler corresponds to the JSON schema field "scheduler". - Scheduler *PluginManifestPermissionsScheduler `json:"scheduler,omitempty" yaml:"scheduler,omitempty" mapstructure:"scheduler,omitempty"` - - // Subsonicapi corresponds to the JSON schema field "subsonicapi". - Subsonicapi *PluginManifestPermissionsSubsonicapi `json:"subsonicapi,omitempty" yaml:"subsonicapi,omitempty" mapstructure:"subsonicapi,omitempty"` - - // Websocket corresponds to the JSON schema field "websocket". - Websocket *PluginManifestPermissionsWebsocket `json:"websocket,omitempty" yaml:"websocket,omitempty" mapstructure:"websocket,omitempty"` - - AdditionalProperties interface{} `mapstructure:",remain"` -} - -// Artwork service permissions -type PluginManifestPermissionsArtwork struct { - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsArtwork) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsArtwork: required") - } - type Plain PluginManifestPermissionsArtwork - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsArtwork(plain) - return nil -} - -// Cache service permissions -type PluginManifestPermissionsCache struct { - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsCache) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsCache: required") - } - type Plain PluginManifestPermissionsCache - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsCache(plain) - return nil -} - -// Configuration service permissions -type PluginManifestPermissionsConfig struct { - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsConfig) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsConfig: required") - } - type Plain PluginManifestPermissionsConfig - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsConfig(plain) - return nil -} - -// HTTP service permissions -type PluginManifestPermissionsHttp struct { - // Whether to allow requests to local/private network addresses - AllowLocalNetwork bool `json:"allowLocalNetwork,omitempty" yaml:"allowLocalNetwork,omitempty" mapstructure:"allowLocalNetwork,omitempty"` - - // Map of URL patterns (e.g., 'https://api.example.com/*') to allowed HTTP - // methods. Redirect destinations must also be included. - AllowedUrls map[string][]PluginManifestPermissionsHttpAllowedUrlsValueElem `json:"allowedUrls" yaml:"allowedUrls" mapstructure:"allowedUrls"` - - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -type PluginManifestPermissionsHttpAllowedUrlsValueElem string - -const PluginManifestPermissionsHttpAllowedUrlsValueElemDELETE PluginManifestPermissionsHttpAllowedUrlsValueElem = "DELETE" -const PluginManifestPermissionsHttpAllowedUrlsValueElemGET PluginManifestPermissionsHttpAllowedUrlsValueElem = "GET" -const PluginManifestPermissionsHttpAllowedUrlsValueElemHEAD PluginManifestPermissionsHttpAllowedUrlsValueElem = "HEAD" -const PluginManifestPermissionsHttpAllowedUrlsValueElemOPTIONS PluginManifestPermissionsHttpAllowedUrlsValueElem = "OPTIONS" -const PluginManifestPermissionsHttpAllowedUrlsValueElemPATCH PluginManifestPermissionsHttpAllowedUrlsValueElem = "PATCH" -const PluginManifestPermissionsHttpAllowedUrlsValueElemPOST PluginManifestPermissionsHttpAllowedUrlsValueElem = "POST" -const PluginManifestPermissionsHttpAllowedUrlsValueElemPUT PluginManifestPermissionsHttpAllowedUrlsValueElem = "PUT" -const PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard PluginManifestPermissionsHttpAllowedUrlsValueElem = "*" - -var enumValues_PluginManifestPermissionsHttpAllowedUrlsValueElem = []interface{}{ - "GET", - "POST", - "PUT", - "DELETE", - "PATCH", - "HEAD", - "OPTIONS", - "*", -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsHttpAllowedUrlsValueElem) UnmarshalJSON(value []byte) error { - var v string - if err := json.Unmarshal(value, &v); err != nil { - return err - } - var ok bool - for _, expected := range enumValues_PluginManifestPermissionsHttpAllowedUrlsValueElem { - if reflect.DeepEqual(v, expected) { - ok = true - break - } - } - if !ok { - return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_PluginManifestPermissionsHttpAllowedUrlsValueElem, v) - } - *j = PluginManifestPermissionsHttpAllowedUrlsValueElem(v) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsHttp) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["allowedUrls"]; raw != nil && !ok { - return fmt.Errorf("field allowedUrls in PluginManifestPermissionsHttp: required") - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsHttp: required") - } - type Plain PluginManifestPermissionsHttp - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if v, ok := raw["allowLocalNetwork"]; !ok || v == nil { - plain.AllowLocalNetwork = false - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsHttp(plain) - return nil -} - -// Scheduler service permissions -type PluginManifestPermissionsScheduler struct { - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsScheduler) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsScheduler: required") - } - type Plain PluginManifestPermissionsScheduler - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsScheduler(plain) - return nil -} - -// SubsonicAPI service permissions -type PluginManifestPermissionsSubsonicapi struct { - // If false, reject calls where the u is an admin - AllowAdmins bool `json:"allowAdmins,omitempty" yaml:"allowAdmins,omitempty" mapstructure:"allowAdmins,omitempty"` - - // List of usernames the plugin can pass as u. Any user if empty - AllowedUsernames []string `json:"allowedUsernames,omitempty" yaml:"allowedUsernames,omitempty" mapstructure:"allowedUsernames,omitempty"` - - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsSubsonicapi) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsSubsonicapi: required") - } - type Plain PluginManifestPermissionsSubsonicapi - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if v, ok := raw["allowAdmins"]; !ok || v == nil { - plain.AllowAdmins = false - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsSubsonicapi(plain) - return nil -} - -// WebSocket service permissions -type PluginManifestPermissionsWebsocket struct { - // Whether to allow connections to local/private network addresses - AllowLocalNetwork bool `json:"allowLocalNetwork,omitempty" yaml:"allowLocalNetwork,omitempty" mapstructure:"allowLocalNetwork,omitempty"` - - // List of WebSocket URL patterns that the plugin is allowed to connect to - AllowedUrls []string `json:"allowedUrls" yaml:"allowedUrls" mapstructure:"allowedUrls"` - - // Explanation of why this permission is needed - Reason string `json:"reason" yaml:"reason" mapstructure:"reason"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifestPermissionsWebsocket) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["allowedUrls"]; raw != nil && !ok { - return fmt.Errorf("field allowedUrls in PluginManifestPermissionsWebsocket: required") - } - if _, ok := raw["reason"]; raw != nil && !ok { - return fmt.Errorf("field reason in PluginManifestPermissionsWebsocket: required") - } - type Plain PluginManifestPermissionsWebsocket - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if v, ok := raw["allowLocalNetwork"]; !ok || v == nil { - plain.AllowLocalNetwork = false - } - if plain.AllowedUrls != nil && len(plain.AllowedUrls) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "allowedUrls", 1) - } - if len(plain.Reason) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "reason", 1) - } - *j = PluginManifestPermissionsWebsocket(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *PluginManifest) UnmarshalJSON(value []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(value, &raw); err != nil { - return err - } - if _, ok := raw["author"]; raw != nil && !ok { - return fmt.Errorf("field author in PluginManifest: required") - } - if _, ok := raw["capabilities"]; raw != nil && !ok { - return fmt.Errorf("field capabilities in PluginManifest: required") - } - if _, ok := raw["description"]; raw != nil && !ok { - return fmt.Errorf("field description in PluginManifest: required") - } - if _, ok := raw["name"]; raw != nil && !ok { - return fmt.Errorf("field name in PluginManifest: required") - } - if _, ok := raw["permissions"]; raw != nil && !ok { - return fmt.Errorf("field permissions in PluginManifest: required") - } - if _, ok := raw["version"]; raw != nil && !ok { - return fmt.Errorf("field version in PluginManifest: required") - } - if _, ok := raw["website"]; raw != nil && !ok { - return fmt.Errorf("field website in PluginManifest: required") - } - type Plain PluginManifest - var plain Plain - if err := json.Unmarshal(value, &plain); err != nil { - return err - } - if plain.Capabilities != nil && len(plain.Capabilities) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "capabilities", 1) - } - *j = PluginManifest(plain) - return nil -} diff --git a/plugins/testdata/.gitignore b/plugins/testdata/.gitignore deleted file mode 100644 index 917660a34..000000000 --- a/plugins/testdata/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.wasm \ No newline at end of file diff --git a/plugins/testdata/Makefile b/plugins/testdata/Makefile deleted file mode 100644 index f569cfce5..000000000 --- a/plugins/testdata/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -# Fake sample plugins used for testing -PLUGINS := fake_album_agent fake_artist_agent fake_scrobbler multi_plugin fake_init_service unauthorized_plugin - -all: $(PLUGINS:%=%/plugin.wasm) - -clean: - rm -f $(PLUGINS:%=%/plugin.wasm) - -%/plugin.wasm: %/plugin.go - GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./$* \ No newline at end of file diff --git a/plugins/testdata/README.md b/plugins/testdata/README.md deleted file mode 100644 index abe840ff8..000000000 --- a/plugins/testdata/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Plugin Test Data - -This directory contains test data and mock implementations used for testing the Navidrome plugin system. - -## Contents - -Each of these directories contains the source code for a simple Go plugin that implements a specific agent interface -(or multiple interfaces in the case of `multi_plugin`). These are compiled into WASM modules using the -`Makefile` and used in integration tests for the plugin adapters (e.g., `adapter_media_agent_test.go`). - -Running `make` within this directory will build all test plugins. - -## Usage - -The primary use of this directory is during the development and testing phase. The `Makefile` is used to build the -necessary WASM plugin binaries. The tests within the `plugins` package (and potentially other packages that interact -with plugins) then utilize these compiled plugins and other test fixtures found here. diff --git a/plugins/testdata/fake_album_agent/manifest.json b/plugins/testdata/fake_album_agent/manifest.json deleted file mode 100644 index e8dfb1fb3..000000000 --- a/plugins/testdata/fake_album_agent/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "fake_album_agent", - "author": "Navidrome Test", - "version": "1.0.0", - "description": "Test data for album agent", - "website": "https://test.navidrome.org/fake-album-agent", - "capabilities": ["MetadataAgent"], - "permissions": {} -} diff --git a/plugins/testdata/fake_album_agent/plugin.go b/plugins/testdata/fake_album_agent/plugin.go deleted file mode 100644 index c35e90397..000000000 --- a/plugins/testdata/fake_album_agent/plugin.go +++ /dev/null @@ -1,70 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - - "github.com/navidrome/navidrome/plugins/api" -) - -type FakeAlbumAgent struct{} - -var ErrNotFound = api.ErrNotFound - -func (FakeAlbumAgent) GetAlbumInfo(ctx context.Context, req *api.AlbumInfoRequest) (*api.AlbumInfoResponse, error) { - if req.Name != "" && req.Artist != "" { - return &api.AlbumInfoResponse{ - Info: &api.AlbumInfo{ - Name: req.Name, - Mbid: "album-mbid-123", - Description: "This is a test album description", - Url: "https://example.com/album", - }, - }, nil - } - return nil, ErrNotFound -} - -func (FakeAlbumAgent) GetAlbumImages(ctx context.Context, req *api.AlbumImagesRequest) (*api.AlbumImagesResponse, error) { - if req.Name != "" && req.Artist != "" { - return &api.AlbumImagesResponse{ - Images: []*api.ExternalImage{ - {Url: "https://example.com/album1.jpg", Size: 300}, - {Url: "https://example.com/album2.jpg", Size: 400}, - }, - }, nil - } - return nil, ErrNotFound -} - -func (FakeAlbumAgent) GetArtistMBID(ctx context.Context, req *api.ArtistMBIDRequest) (*api.ArtistMBIDResponse, error) { - return nil, api.ErrNotImplemented -} - -func (FakeAlbumAgent) GetArtistURL(ctx context.Context, req *api.ArtistURLRequest) (*api.ArtistURLResponse, error) { - return nil, api.ErrNotImplemented -} - -func (FakeAlbumAgent) GetArtistBiography(ctx context.Context, req *api.ArtistBiographyRequest) (*api.ArtistBiographyResponse, error) { - return nil, api.ErrNotImplemented -} - -func (FakeAlbumAgent) GetSimilarArtists(ctx context.Context, req *api.ArtistSimilarRequest) (*api.ArtistSimilarResponse, error) { - return nil, api.ErrNotImplemented -} - -func (FakeAlbumAgent) GetArtistImages(ctx context.Context, req *api.ArtistImageRequest) (*api.ArtistImageResponse, error) { - return nil, api.ErrNotImplemented -} - -func (FakeAlbumAgent) GetArtistTopSongs(ctx context.Context, req *api.ArtistTopSongsRequest) (*api.ArtistTopSongsResponse, error) { - return nil, api.ErrNotImplemented -} - -func main() {} - -// Register the plugin implementation -func init() { - api.RegisterMetadataAgent(FakeAlbumAgent{}) -} diff --git a/plugins/testdata/fake_artist_agent/manifest.json b/plugins/testdata/fake_artist_agent/manifest.json deleted file mode 100644 index c5db72565..000000000 --- a/plugins/testdata/fake_artist_agent/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "fake_artist_agent", - "author": "Navidrome Test", - "version": "1.0.0", - "description": "Test data for artist agent", - "website": "https://test.navidrome.org/fake-artist-agent", - "capabilities": ["MetadataAgent"], - "permissions": {} -} diff --git a/plugins/testdata/fake_artist_agent/plugin.go b/plugins/testdata/fake_artist_agent/plugin.go deleted file mode 100644 index bd6b0f771..000000000 --- a/plugins/testdata/fake_artist_agent/plugin.go +++ /dev/null @@ -1,82 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - - "github.com/navidrome/navidrome/plugins/api" -) - -type FakeArtistAgent struct{} - -var ErrNotFound = api.ErrNotFound - -func (FakeArtistAgent) GetArtistMBID(ctx context.Context, req *api.ArtistMBIDRequest) (*api.ArtistMBIDResponse, error) { - if req.Name != "" { - return &api.ArtistMBIDResponse{Mbid: "1234567890"}, nil - } - return nil, ErrNotFound -} -func (FakeArtistAgent) GetArtistURL(ctx context.Context, req *api.ArtistURLRequest) (*api.ArtistURLResponse, error) { - if req.Name != "" { - return &api.ArtistURLResponse{Url: "https://example.com"}, nil - } - return nil, ErrNotFound -} -func (FakeArtistAgent) GetArtistBiography(ctx context.Context, req *api.ArtistBiographyRequest) (*api.ArtistBiographyResponse, error) { - if req.Name != "" { - return &api.ArtistBiographyResponse{Biography: "This is a test biography"}, nil - } - return nil, ErrNotFound -} -func (FakeArtistAgent) GetSimilarArtists(ctx context.Context, req *api.ArtistSimilarRequest) (*api.ArtistSimilarResponse, error) { - if req.Name != "" { - return &api.ArtistSimilarResponse{ - Artists: []*api.Artist{ - {Name: "Similar Artist 1", Mbid: "mbid1"}, - {Name: "Similar Artist 2", Mbid: "mbid2"}, - }, - }, nil - } - return nil, ErrNotFound -} -func (FakeArtistAgent) GetArtistImages(ctx context.Context, req *api.ArtistImageRequest) (*api.ArtistImageResponse, error) { - if req.Name != "" { - return &api.ArtistImageResponse{ - Images: []*api.ExternalImage{ - {Url: "https://example.com/image1.jpg", Size: 100}, - {Url: "https://example.com/image2.jpg", Size: 200}, - }, - }, nil - } - return nil, ErrNotFound -} -func (FakeArtistAgent) GetArtistTopSongs(ctx context.Context, req *api.ArtistTopSongsRequest) (*api.ArtistTopSongsResponse, error) { - if req.ArtistName != "" { - return &api.ArtistTopSongsResponse{ - Songs: []*api.Song{ - {Name: "Song 1", Mbid: "mbid1"}, - {Name: "Song 2", Mbid: "mbid2"}, - }, - }, nil - } - return nil, ErrNotFound -} - -// Add empty implementations for the album methods to satisfy the MetadataAgent interface -func (FakeArtistAgent) GetAlbumInfo(ctx context.Context, req *api.AlbumInfoRequest) (*api.AlbumInfoResponse, error) { - return nil, api.ErrNotImplemented -} - -func (FakeArtistAgent) GetAlbumImages(ctx context.Context, req *api.AlbumImagesRequest) (*api.AlbumImagesResponse, error) { - return nil, api.ErrNotImplemented -} - -// main is required by Go WASI build -func main() {} - -// init is used by go-plugin to register the implementation -func init() { - api.RegisterMetadataAgent(FakeArtistAgent{}) -} diff --git a/plugins/testdata/fake_init_service/manifest.json b/plugins/testdata/fake_init_service/manifest.json deleted file mode 100644 index ea8c45f58..000000000 --- a/plugins/testdata/fake_init_service/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "fake_init_service", - "version": "1.0.0", - "capabilities": ["LifecycleManagement"], - "author": "Test Author", - "description": "Test LifecycleManagement Callback", - "website": "https://test.navidrome.org/fake-init-service", - "permissions": {} -} diff --git a/plugins/testdata/fake_init_service/plugin.go b/plugins/testdata/fake_init_service/plugin.go deleted file mode 100644 index 9e6171623..000000000 --- a/plugins/testdata/fake_init_service/plugin.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "errors" - "log" - - "github.com/navidrome/navidrome/plugins/api" -) - -type initServicePlugin struct{} - -func (p *initServicePlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { - log.Printf("OnInit called with %v", req) - - // Check for specific error conditions in the config - if req.Config != nil { - if errorType, exists := req.Config["returnError"]; exists { - switch errorType { - case "go_error": - return nil, errors.New("initialization failed with Go error") - case "response_error": - return &api.InitResponse{ - Error: "initialization failed with response error", - }, nil - } - } - } - - // Default: successful initialization - return &api.InitResponse{}, nil -} - -// Required by Go WASI build -func main() {} - -// Register the LifecycleManagement implementation -func init() { - api.RegisterLifecycleManagement(&initServicePlugin{}) -} diff --git a/plugins/testdata/fake_scrobbler/manifest.json b/plugins/testdata/fake_scrobbler/manifest.json deleted file mode 100644 index 6fa41aa31..000000000 --- a/plugins/testdata/fake_scrobbler/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "fake_scrobbler", - "author": "Navidrome Test", - "version": "1.0.0", - "description": "Test data for scrobbler", - "website": "https://test.navidrome.org/fake-scrobbler", - "capabilities": ["Scrobbler"], - "permissions": {} -} diff --git a/plugins/testdata/fake_scrobbler/plugin.go b/plugins/testdata/fake_scrobbler/plugin.go deleted file mode 100644 index 5a5c76699..000000000 --- a/plugins/testdata/fake_scrobbler/plugin.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "log" - - "github.com/navidrome/navidrome/plugins/api" -) - -type FakeScrobbler struct{} - -func (FakeScrobbler) IsAuthorized(ctx context.Context, req *api.ScrobblerIsAuthorizedRequest) (*api.ScrobblerIsAuthorizedResponse, error) { - log.Printf("[FakeScrobbler] IsAuthorized called for user: %s (%s)", req.Username, req.UserId) - return &api.ScrobblerIsAuthorizedResponse{Authorized: true}, nil -} - -func (FakeScrobbler) NowPlaying(ctx context.Context, req *api.ScrobblerNowPlayingRequest) (*api.ScrobblerNowPlayingResponse, error) { - log.Printf("[FakeScrobbler] NowPlaying called for user: %s (%s), track: %s", req.Username, req.UserId, req.Track.Name) - return &api.ScrobblerNowPlayingResponse{}, nil -} - -func (FakeScrobbler) Scrobble(ctx context.Context, req *api.ScrobblerScrobbleRequest) (*api.ScrobblerScrobbleResponse, error) { - log.Printf("[FakeScrobbler] Scrobble called for user: %s (%s), track: %s, timestamp: %d", req.Username, req.UserId, req.Track.Name, req.Timestamp) - return &api.ScrobblerScrobbleResponse{}, nil -} - -func main() {} - -func init() { - api.RegisterScrobbler(FakeScrobbler{}) -} diff --git a/plugins/testdata/multi_plugin/manifest.json b/plugins/testdata/multi_plugin/manifest.json deleted file mode 100644 index dc9e0a9a8..000000000 --- a/plugins/testdata/multi_plugin/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "multi_plugin", - "author": "Navidrome Test", - "version": "1.0.0", - "description": "Test data for multiple services", - "website": "https://test.navidrome.org/multi-plugin", - "capabilities": ["MetadataAgent", "SchedulerCallback", "LifecycleManagement"], - "permissions": { - "scheduler": { - "reason": "For testing scheduled callback functionality" - } - } -} diff --git a/plugins/testdata/multi_plugin/plugin.go b/plugins/testdata/multi_plugin/plugin.go deleted file mode 100644 index 3c28bd214..000000000 --- a/plugins/testdata/multi_plugin/plugin.go +++ /dev/null @@ -1,124 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - "log" - "strings" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/scheduler" -) - -// MultiPlugin implements the MetadataAgent interface for testing -type MultiPlugin struct{} - -var ErrNotFound = api.ErrNotFound - -var sched = scheduler.NewSchedulerService() - -// Artist-related methods -func (MultiPlugin) GetArtistMBID(ctx context.Context, req *api.ArtistMBIDRequest) (*api.ArtistMBIDResponse, error) { - if req.Name != "" { - return &api.ArtistMBIDResponse{Mbid: "multi-artist-mbid"}, nil - } - return nil, ErrNotFound -} - -func (MultiPlugin) GetArtistURL(ctx context.Context, req *api.ArtistURLRequest) (*api.ArtistURLResponse, error) { - log.Printf("GetArtistURL received: %v", req) - - // Use an ID that could potentially clash with other plugins - // The host will ensure this doesn't conflict by prefixing with plugin name - customId := "artist:" + req.Name - log.Printf("Registering scheduler with custom ID: %s", customId) - - // Use the scheduler service for one-time scheduling - resp, err := sched.ScheduleOneTime(ctx, &scheduler.ScheduleOneTimeRequest{ - ScheduleId: customId, - DelaySeconds: 6, - Payload: []byte("test-payload"), - }) - if err != nil { - log.Printf("Error scheduling one-time job: %v", err) - } else { - log.Printf("One-time schedule registered with ID: %s", resp.ScheduleId) - } - - return &api.ArtistURLResponse{Url: "https://multi.example.com/artist"}, nil -} - -func (MultiPlugin) GetArtistBiography(ctx context.Context, req *api.ArtistBiographyRequest) (*api.ArtistBiographyResponse, error) { - return &api.ArtistBiographyResponse{Biography: "Multi agent artist bio"}, nil -} - -func (MultiPlugin) GetSimilarArtists(ctx context.Context, req *api.ArtistSimilarRequest) (*api.ArtistSimilarResponse, error) { - return &api.ArtistSimilarResponse{}, nil -} - -func (MultiPlugin) GetArtistImages(ctx context.Context, req *api.ArtistImageRequest) (*api.ArtistImageResponse, error) { - return &api.ArtistImageResponse{}, nil -} - -func (MultiPlugin) GetArtistTopSongs(ctx context.Context, req *api.ArtistTopSongsRequest) (*api.ArtistTopSongsResponse, error) { - return &api.ArtistTopSongsResponse{}, nil -} - -// Album-related methods -func (MultiPlugin) GetAlbumInfo(ctx context.Context, req *api.AlbumInfoRequest) (*api.AlbumInfoResponse, error) { - if req.Name != "" && req.Artist != "" { - return &api.AlbumInfoResponse{ - Info: &api.AlbumInfo{ - Name: req.Name, - Mbid: "multi-album-mbid", - Description: "Multi agent album description", - Url: "https://multi.example.com/album", - }, - }, nil - } - return nil, ErrNotFound -} - -func (MultiPlugin) GetAlbumImages(ctx context.Context, req *api.AlbumImagesRequest) (*api.AlbumImagesResponse, error) { - return &api.AlbumImagesResponse{}, nil -} - -// Scheduler callback -func (MultiPlugin) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) { - log.Printf("Scheduler callback received with ID: %s, payload: '%s', isRecurring: %v", - req.ScheduleId, string(req.Payload), req.IsRecurring) - - // Demonstrate how to parse the custom ID format - if strings.HasPrefix(req.ScheduleId, "artist:") { - parts := strings.Split(req.ScheduleId, ":") - if len(parts) == 2 { - artistName := parts[1] - log.Printf("This schedule was for artist: %s", artistName) - } - } - - return &api.SchedulerCallbackResponse{}, nil -} - -func (MultiPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { - log.Printf("OnInit called with %v", req) - - // Schedule a recurring every 5 seconds - _, _ = sched.ScheduleRecurring(ctx, &scheduler.ScheduleRecurringRequest{ - CronExpression: "@every 5s", - Payload: []byte("every 5 seconds"), - }) - - return &api.InitResponse{}, nil -} - -// Required by Go WASI build -func main() {} - -// Register the service implementations -func init() { - api.RegisterLifecycleManagement(MultiPlugin{}) - api.RegisterMetadataAgent(MultiPlugin{}) - api.RegisterSchedulerCallback(MultiPlugin{}) -} diff --git a/plugins/testdata/unauthorized_plugin/manifest.json b/plugins/testdata/unauthorized_plugin/manifest.json deleted file mode 100644 index 38a00e0ea..000000000 --- a/plugins/testdata/unauthorized_plugin/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "unauthorized_plugin", - "author": "Navidrome Test", - "version": "1.0.0", - "description": "Test plugin that tries to access unauthorized services", - "website": "https://test.navidrome.org/unauthorized-plugin", - "capabilities": ["MetadataAgent"], - "permissions": {} -} diff --git a/plugins/testdata/unauthorized_plugin/plugin.go b/plugins/testdata/unauthorized_plugin/plugin.go deleted file mode 100644 index 07c3e0f6b..000000000 --- a/plugins/testdata/unauthorized_plugin/plugin.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build wasip1 - -package main - -import ( - "context" - - "github.com/navidrome/navidrome/plugins/api" - "github.com/navidrome/navidrome/plugins/host/http" -) - -type UnauthorizedPlugin struct{} - -var ErrNotFound = api.ErrNotFound - -func (UnauthorizedPlugin) GetAlbumInfo(ctx context.Context, req *api.AlbumInfoRequest) (*api.AlbumInfoResponse, error) { - // This plugin attempts to make an HTTP call without having HTTP permission - // This should fail since the plugin has no permissions in its manifest - httpClient := http.NewHttpService() - - request := &http.HttpRequest{ - Url: "https://example.com/test", - Headers: map[string]string{ - "Accept": "application/json", - }, - TimeoutMs: 5000, - } - - _, err := httpClient.Get(ctx, request) - if err != nil { - // Expected to fail due to missing permission - return nil, err - } - - return &api.AlbumInfoResponse{ - Info: &api.AlbumInfo{ - Name: req.Name, - Mbid: "unauthorized-test", - Description: "This should not work", - Url: "https://example.com/unauthorized", - }, - }, nil -} - -func (UnauthorizedPlugin) GetAlbumImages(ctx context.Context, req *api.AlbumImagesRequest) (*api.AlbumImagesResponse, error) { - return nil, api.ErrNotImplemented -} - -func (UnauthorizedPlugin) GetArtistMBID(ctx context.Context, req *api.ArtistMBIDRequest) (*api.ArtistMBIDResponse, error) { - return nil, api.ErrNotImplemented -} - -func (UnauthorizedPlugin) GetArtistURL(ctx context.Context, req *api.ArtistURLRequest) (*api.ArtistURLResponse, error) { - return nil, api.ErrNotImplemented -} - -func (UnauthorizedPlugin) GetArtistBiography(ctx context.Context, req *api.ArtistBiographyRequest) (*api.ArtistBiographyResponse, error) { - return nil, api.ErrNotImplemented -} - -func (UnauthorizedPlugin) GetSimilarArtists(ctx context.Context, req *api.ArtistSimilarRequest) (*api.ArtistSimilarResponse, error) { - return nil, api.ErrNotImplemented -} - -func (UnauthorizedPlugin) GetArtistImages(ctx context.Context, req *api.ArtistImageRequest) (*api.ArtistImageResponse, error) { - return nil, api.ErrNotImplemented -} - -func (UnauthorizedPlugin) GetArtistTopSongs(ctx context.Context, req *api.ArtistTopSongsRequest) (*api.ArtistTopSongsResponse, error) { - return nil, api.ErrNotImplemented -} - -func main() {} - -// Register the plugin implementation -func init() { - api.RegisterMetadataAgent(UnauthorizedPlugin{}) -} diff --git a/plugins/wasm_instance_pool.go b/plugins/wasm_instance_pool.go deleted file mode 100644 index 5ea1a82a6..000000000 --- a/plugins/wasm_instance_pool.go +++ /dev/null @@ -1,223 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/navidrome/navidrome/log" -) - -// wasmInstancePool is a generic pool using channels for simplicity and Go idioms -type wasmInstancePool[T any] struct { - name string - new func(ctx context.Context) (T, error) - poolSize int - getTimeout time.Duration - ttl time.Duration - - mu sync.RWMutex - instances chan poolItem[T] - semaphore chan struct{} - closing chan struct{} - closed bool -} - -type poolItem[T any] struct { - value T - created time.Time -} - -func newWasmInstancePool[T any](name string, poolSize int, maxConcurrentInstances int, getTimeout time.Duration, ttl time.Duration, newFn func(ctx context.Context) (T, error)) *wasmInstancePool[T] { - p := &wasmInstancePool[T]{ - name: name, - new: newFn, - poolSize: poolSize, - getTimeout: getTimeout, - ttl: ttl, - instances: make(chan poolItem[T], poolSize), - semaphore: make(chan struct{}, maxConcurrentInstances), - closing: make(chan struct{}), - } - - // Fill semaphore to allow maxConcurrentInstances - for i := 0; i < maxConcurrentInstances; i++ { - p.semaphore <- struct{}{} - } - - log.Debug(context.Background(), "wasmInstancePool: created new pool", "pool", p.name, "poolSize", p.poolSize, "maxConcurrentInstances", maxConcurrentInstances, "getTimeout", p.getTimeout, "ttl", p.ttl) - go p.cleanupLoop() - return p -} - -func getInstanceID(inst any) string { - return fmt.Sprintf("%p", inst) //nolint:govet -} - -func (p *wasmInstancePool[T]) Get(ctx context.Context) (T, error) { - // First acquire a semaphore slot (concurrent limit) - select { - case <-p.semaphore: - // Got slot, continue - case <-ctx.Done(): - var zero T - return zero, ctx.Err() - case <-time.After(p.getTimeout): - var zero T - return zero, fmt.Errorf("timeout waiting for available instance after %v", p.getTimeout) - case <-p.closing: - var zero T - return zero, fmt.Errorf("pool is closing") - } - - // Try to get from pool first - p.mu.RLock() - instances := p.instances - p.mu.RUnlock() - - select { - case item := <-instances: - log.Trace(ctx, "wasmInstancePool: got instance from pool", "pool", p.name, "instanceID", getInstanceID(item.value)) - return item.value, nil - default: - // Pool empty, create new instance - instance, err := p.new(ctx) - if err != nil { - // Failed to create, return semaphore slot - log.Trace(ctx, "wasmInstancePool: failed to create new instance", "pool", p.name, err) - p.semaphore <- struct{}{} - var zero T - return zero, err - } - log.Trace(ctx, "wasmInstancePool: new instance created", "pool", p.name, "instanceID", getInstanceID(instance)) - return instance, nil - } -} - -func (p *wasmInstancePool[T]) Put(ctx context.Context, v T) { - p.mu.RLock() - instances := p.instances - closed := p.closed - p.mu.RUnlock() - - if closed { - log.Trace(ctx, "wasmInstancePool: pool closed, closing instance", "pool", p.name, "instanceID", getInstanceID(v)) - p.closeItem(ctx, v) - // Return semaphore slot only if this instance came from Get() - select { - case p.semaphore <- struct{}{}: - case <-p.closing: - default: - // Semaphore full, this instance didn't come from Get() - } - return - } - - // Try to return to pool - item := poolItem[T]{value: v, created: time.Now()} - select { - case instances <- item: - log.Trace(ctx, "wasmInstancePool: returned instance to pool", "pool", p.name, "instanceID", getInstanceID(v)) - default: - // Pool full, close instance - log.Trace(ctx, "wasmInstancePool: pool full, closing instance", "pool", p.name, "instanceID", getInstanceID(v)) - p.closeItem(ctx, v) - } - - // Return semaphore slot only if this instance came from Get() - // If semaphore is full, this instance didn't come from Get(), so don't block - select { - case p.semaphore <- struct{}{}: - // Successfully returned token - case <-p.closing: - // Pool closing, don't block - default: - // Semaphore full, this instance didn't come from Get() - } -} - -func (p *wasmInstancePool[T]) Close(ctx context.Context) { - p.mu.Lock() - if p.closed { - p.mu.Unlock() - return - } - p.closed = true - close(p.closing) - instances := p.instances - p.mu.Unlock() - - log.Trace(ctx, "wasmInstancePool: closing pool and all instances", "pool", p.name) - - // Drain and close all instances - for { - select { - case item := <-instances: - p.closeItem(ctx, item.value) - default: - return - } - } -} - -func (p *wasmInstancePool[T]) cleanupLoop() { - ticker := time.NewTicker(p.ttl / 3) - defer ticker.Stop() - for { - select { - case <-ticker.C: - p.cleanupExpired() - case <-p.closing: - return - } - } -} - -func (p *wasmInstancePool[T]) cleanupExpired() { - ctx := context.Background() - now := time.Now() - - // Create new channel with same capacity - newInstances := make(chan poolItem[T], p.poolSize) - - // Atomically swap channels - p.mu.Lock() - oldInstances := p.instances - p.instances = newInstances - p.mu.Unlock() - - // Drain old channel, keeping fresh items - var expiredCount int - for { - select { - case item := <-oldInstances: - if now.Sub(item.created) <= p.ttl { - // Item is still fresh, move to new channel - select { - case newInstances <- item: - // Successfully moved - default: - // New channel full, close excess item - p.closeItem(ctx, item.value) - } - } else { - // Item expired, close it - expiredCount++ - p.closeItem(ctx, item.value) - } - default: - // Old channel drained - if expiredCount > 0 { - log.Trace(ctx, "wasmInstancePool: cleaned up expired instances", "pool", p.name, "expiredCount", expiredCount) - } - return - } - } -} - -func (p *wasmInstancePool[T]) closeItem(ctx context.Context, v T) { - if closer, ok := any(v).(interface{ Close(context.Context) error }); ok { - _ = closer.Close(ctx) - } -} diff --git a/plugins/wasm_instance_pool_test.go b/plugins/wasm_instance_pool_test.go deleted file mode 100644 index 141210473..000000000 --- a/plugins/wasm_instance_pool_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package plugins - -import ( - "context" - "sync/atomic" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -type testInstance struct { - closed atomic.Bool -} - -func (t *testInstance) Close(ctx context.Context) error { - t.closed.Store(true) - return nil -} - -var _ = Describe("wasmInstancePool", func() { - var ( - ctx = context.Background() - ) - - It("should Get and Put instances", func() { - pool := newWasmInstancePool[*testInstance]("test", 2, 10, 5*time.Second, time.Second, func(ctx context.Context) (*testInstance, error) { - return &testInstance{}, nil - }) - inst, err := pool.Get(ctx) - Expect(err).To(BeNil()) - Expect(inst).ToNot(BeNil()) - pool.Put(ctx, inst) - inst2, err := pool.Get(ctx) - Expect(err).To(BeNil()) - Expect(inst2).To(Equal(inst)) - pool.Close(ctx) - }) - - It("should not exceed max instances", func() { - pool := newWasmInstancePool[*testInstance]("test", 1, 10, 5*time.Second, time.Second, func(ctx context.Context) (*testInstance, error) { - return &testInstance{}, nil - }) - inst1, err := pool.Get(ctx) - Expect(err).To(BeNil()) - inst2 := &testInstance{} - pool.Put(ctx, inst1) - pool.Put(ctx, inst2) // should close inst2 - Expect(inst2.closed.Load()).To(BeTrue()) - pool.Close(ctx) - }) - - It("should expire and close instances after TTL", func() { - pool := newWasmInstancePool[*testInstance]("test", 2, 10, 5*time.Second, 100*time.Millisecond, func(ctx context.Context) (*testInstance, error) { - return &testInstance{}, nil - }) - inst, err := pool.Get(ctx) - Expect(err).To(BeNil()) - pool.Put(ctx, inst) - // Wait for TTL cleanup - time.Sleep(300 * time.Millisecond) - Expect(inst.closed.Load()).To(BeTrue()) - pool.Close(ctx) - }) - - It("should close all on pool Close", func() { - pool := newWasmInstancePool[*testInstance]("test", 2, 10, 5*time.Second, time.Second, func(ctx context.Context) (*testInstance, error) { - return &testInstance{}, nil - }) - inst1, err := pool.Get(ctx) - Expect(err).To(BeNil()) - inst2, err := pool.Get(ctx) - Expect(err).To(BeNil()) - pool.Put(ctx, inst1) - pool.Put(ctx, inst2) - pool.Close(ctx) - Expect(inst1.closed.Load()).To(BeTrue()) - Expect(inst2.closed.Load()).To(BeTrue()) - }) - - It("should be safe for concurrent Get/Put", func() { - pool := newWasmInstancePool[*testInstance]("test", 4, 10, 5*time.Second, time.Second, func(ctx context.Context) (*testInstance, error) { - return &testInstance{}, nil - }) - done := make(chan struct{}) - for i := 0; i < 8; i++ { - go func() { - inst, err := pool.Get(ctx) - Expect(err).To(BeNil()) - pool.Put(ctx, inst) - done <- struct{}{} - }() - } - for i := 0; i < 8; i++ { - <-done - } - pool.Close(ctx) - }) - - It("should enforce max concurrent instances limit", func() { - callCount := atomic.Int32{} - pool := newWasmInstancePool[*testInstance]("test", 2, 3, 100*time.Millisecond, time.Second, func(ctx context.Context) (*testInstance, error) { - callCount.Add(1) - return &testInstance{}, nil - }) - - // Get 3 instances (should hit the limit) - inst1, err := pool.Get(ctx) - Expect(err).To(BeNil()) - inst2, err := pool.Get(ctx) - Expect(err).To(BeNil()) - inst3, err := pool.Get(ctx) - Expect(err).To(BeNil()) - - // Should have created exactly 3 instances at this point - Expect(callCount.Load()).To(Equal(int32(3))) - - // Fourth call should timeout without creating a new instance - start := time.Now() - _, err = pool.Get(ctx) - duration := time.Since(start) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("timeout waiting for available instance")) - Expect(duration).To(BeNumerically(">=", 100*time.Millisecond)) - Expect(duration).To(BeNumerically("<", 200*time.Millisecond)) - - // Still should have only 3 instances (timeout didn't create new one) - Expect(callCount.Load()).To(Equal(int32(3))) - - // Return one instance and try again - should succeed by reusing returned instance - pool.Put(ctx, inst1) - inst4, err := pool.Get(ctx) - Expect(err).To(BeNil()) - Expect(inst4).To(Equal(inst1)) // Should be the same instance we returned - - // Still should have only 3 instances total (reused inst1) - Expect(callCount.Load()).To(Equal(int32(3))) - - pool.Put(ctx, inst2) - pool.Put(ctx, inst3) - pool.Put(ctx, inst4) - pool.Close(ctx) - }) - - It("should handle concurrent waiters properly", func() { - pool := newWasmInstancePool[*testInstance]("test", 1, 2, time.Second, time.Second, func(ctx context.Context) (*testInstance, error) { - return &testInstance{}, nil - }) - - // Fill up the concurrent slots - inst1, err := pool.Get(ctx) - Expect(err).To(BeNil()) - inst2, err := pool.Get(ctx) - Expect(err).To(BeNil()) - - // Start multiple waiters - waiterResults := make(chan error, 3) - for i := 0; i < 3; i++ { - go func() { - _, err := pool.Get(ctx) - waiterResults <- err - }() - } - - // Wait a bit to ensure waiters are queued - time.Sleep(50 * time.Millisecond) - - // Return instances one by one - pool.Put(ctx, inst1) - pool.Put(ctx, inst2) - - // Two waiters should succeed, one should timeout - successCount := 0 - timeoutCount := 0 - for i := 0; i < 3; i++ { - select { - case err := <-waiterResults: - if err == nil { - successCount++ - } else { - timeoutCount++ - } - case <-time.After(2 * time.Second): - Fail("Test timed out waiting for waiter results") - } - } - - Expect(successCount).To(Equal(2)) - Expect(timeoutCount).To(Equal(1)) - - pool.Close(ctx) - }) -})