package imdb import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/tools" ) // SearchResult represents a single movie/series result from OMDb API. type SearchResult struct { Title string `json:"Title"` Year string `json:"Year"` ImdbID string `json:"imdbID"` Type string `json:"Type"` Poster string `json:"Poster"` } // SearchResponse represents the full response from OMDb search endpoint. type SearchResponse struct { Search []SearchResult `json:"Search"` TotalResults string `json:"totalResults"` Response string `json:"Response"` Error string `json:"Error"` } // NewIMDbSearch creates an imdb_search tool that searches movies on IMDb via OMDb API. // Returns up to 5 results with title, year, type, poster URL, and IMDb ID. // Requires API key from http://www.omdbapi.com/ func NewIMDbSearch(cfg config.IMDbToolCfg) tools.Tool { timeout := cfg.Timeout if timeout == 0 { timeout = 10 * time.Second } client := &http.Client{Timeout: timeout} return tools.Tool{ Def: tools.Def{ Name: "imdb_search", Description: "Search for movies or series on IMDb by title. Returns up to 5 results with title, year, type, poster image URL, and IMDb ID.", Parameters: []tools.Param{ {Name: "query", Type: "string", Description: "The movie or series title to search for", Required: true}, {Name: "year", Type: "integer", Description: "Optional year to filter results (e.g., 2020)", Required: false}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { query := tools.GetString(args, "query") if query == "" { return tools.Result{Err: fmt.Errorf("imdb_search: query is required")} } // Get API key from config or env var apiKey := cfg.APIKey if apiKey == "" && cfg.APIKeyEnv != "" { apiKey = getEnvVar(cfg.APIKeyEnv) } if apiKey == "" { return tools.Result{Err: fmt.Errorf("imdb_search: API key not configured (set imdb.api_key or imdb.api_key_env in config)")} } // Build search URL searchURL := buildSearchURL(apiKey, query, args) req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil) if err != nil { return tools.Result{Err: fmt.Errorf("imdb_search: %w", err)} } resp, err := client.Do(req) if err != nil { return tools.Result{Err: fmt.Errorf("imdb_search: %w", err)} } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return tools.Result{Err: fmt.Errorf("imdb_search: HTTP %d", resp.StatusCode)} } body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) if err != nil { return tools.Result{Err: fmt.Errorf("imdb_search: read body: %w", err)} } var searchResp SearchResponse if err := json.Unmarshal(body, &searchResp); err != nil { return tools.Result{Err: fmt.Errorf("imdb_search: parse response: %w", err)} } if searchResp.Response == "False" { return tools.Result{Output: fmt.Sprintf("No se encontraron resultados para '%s'. Error: %s", query, searchResp.Error)} } // Format results (limit to first 5) output := formatResults(searchResp.Search, query) return tools.Result{Output: output} }, } } // buildSearchURL constructs the OMDb API search URL with query parameters. func buildSearchURL(apiKey, query string, args map[string]any) string { params := url.Values{} params.Set("apikey", apiKey) params.Set("s", query) params.Set("type", "movie") // default to movies, could be made configurable // Add year filter if provided if year := tools.GetInt(args, "year"); year > 0 { params.Set("y", fmt.Sprintf("%d", year)) } return fmt.Sprintf("https://www.omdbapi.com/?%s", params.Encode()) } // formatResults converts search results into a readable text format. func formatResults(results []SearchResult, query string) string { if len(results) == 0 { return fmt.Sprintf("No se encontraron películas para '%s'", query) } var builder strings.Builder builder.WriteString(fmt.Sprintf("🎬 Resultados de IMDb para '%s':\n\n", query)) // Limit to 5 results limit := 5 if len(results) < limit { limit = len(results) } for i := 0; i < limit; i++ { r := results[i] builder.WriteString(fmt.Sprintf("%d. **%s** (%s)\n", i+1, r.Title, r.Year)) builder.WriteString(fmt.Sprintf(" • Tipo: %s\n", r.Type)) builder.WriteString(fmt.Sprintf(" • IMDb ID: %s\n", r.ImdbID)) if r.Poster != "" && r.Poster != "N/A" { builder.WriteString(fmt.Sprintf(" • Poster: %s\n", r.Poster)) } else { builder.WriteString(" • Poster: No disponible\n") } builder.WriteString(fmt.Sprintf(" • Link: https://www.imdb.com/title/%s/\n", r.ImdbID)) if i < limit-1 { builder.WriteString("\n") } } if len(results) > 5 { builder.WriteString(fmt.Sprintf("\n... y %d resultado(s) más", len(results)-5)) } return builder.String() } // getEnvVar retrieves an environment variable by name. func getEnvVar(name string) string { return os.Getenv(name) }