Init
This commit is contained in:
commit
d0187f94d7
23 changed files with 2489 additions and 0 deletions
242
spm/search.go
Normal file
242
spm/search.go
Normal file
|
@ -0,0 +1,242 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Addon struct capturing fields from AMO search results (v5)
|
||||
type Addon struct {
|
||||
ID int `json:"id"`
|
||||
Name map[string]string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
URL string `json:"url"`
|
||||
|
||||
CurrentVersion struct {
|
||||
Version string `json:"version"`
|
||||
ReleaseNotes map[string]string `json:"release_notes"`
|
||||
Files []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"files"`
|
||||
} `json:"current_version"`
|
||||
|
||||
Author struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"author"`
|
||||
|
||||
IconURL string `json:"icon_url"`
|
||||
Summary map[string]string `json:"summary"`
|
||||
Tags []string `json:"tags"`
|
||||
License struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"license"`
|
||||
|
||||
// Make sure we match the real AMO JSON: "image_url" is where
|
||||
// the full-size preview is stored (not "url")
|
||||
Previews []struct {
|
||||
ID int `json:"id"`
|
||||
Caption string `json:"caption"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
} `json:"previews"`
|
||||
|
||||
ThemeData struct {
|
||||
Images struct {
|
||||
Header string `json:"header"`
|
||||
} `json:"images"`
|
||||
} `json:"theme_data"`
|
||||
}
|
||||
|
||||
// AMOResponse is the structure returned by the AMO search endpoint (v5).
|
||||
type AMOResponse struct {
|
||||
Count int `json:"count"`
|
||||
Results []Addon `json:"results"`
|
||||
}
|
||||
|
||||
func searchMozillaAddons(query, addonType string) ([]AppIndexEntry, error) {
|
||||
baseURL := "https://addons.mozilla.org/api/v5/addons/search/"
|
||||
q := url.QueryEscape(query)
|
||||
apiURL := fmt.Sprintf("%s?app=firefox&q=%s", baseURL, q)
|
||||
if addonType != "" && addonType != "all" {
|
||||
apiURL += "&type=" + url.QueryEscape(addonType)
|
||||
}
|
||||
|
||||
log.Printf("AMO API request: %s", apiURL)
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed building request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "MyCustomBrowser/1.0")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query AMO: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("AMO returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed reading AMO response: %w", err)
|
||||
}
|
||||
|
||||
var amoResp AMOResponse
|
||||
if err := json.Unmarshal(bodyBytes, &amoResp); err != nil {
|
||||
return nil, fmt.Errorf("failed parsing AMO JSON: %w", err)
|
||||
}
|
||||
|
||||
var entries []AppIndexEntry
|
||||
for _, a := range amoResp.Results {
|
||||
// Determine display name.
|
||||
var addonName string
|
||||
if val, ok := a.Name["en-US"]; ok && val != "" {
|
||||
addonName = val
|
||||
} else {
|
||||
for _, v := range a.Name {
|
||||
addonName = v
|
||||
break
|
||||
}
|
||||
if addonName == "" {
|
||||
addonName = "Unknown Add-on"
|
||||
}
|
||||
}
|
||||
|
||||
finalType := addonType
|
||||
if finalType == "" || finalType == "all" {
|
||||
finalType = "addon"
|
||||
}
|
||||
|
||||
// Pick download URL from current version if present.
|
||||
downloadURL := a.URL
|
||||
if len(a.CurrentVersion.Files) > 0 && a.CurrentVersion.Files[0].URL != "" {
|
||||
downloadURL = a.CurrentVersion.Files[0].URL
|
||||
}
|
||||
|
||||
// Gather screenshot URLs from Previews using preview.ImageURL.
|
||||
var screenshots []string
|
||||
for _, preview := range a.Previews {
|
||||
screenshots = append(screenshots, preview.ImageURL)
|
||||
}
|
||||
|
||||
// If this is a theme, override screenshots to first preview.
|
||||
if strings.EqualFold(finalType, "theme") || strings.EqualFold(finalType, "statictheme") ||
|
||||
strings.EqualFold(addonType, "theme") || strings.EqualFold(addonType, "statictheme") {
|
||||
if len(a.Previews) > 0 {
|
||||
log.Printf("Using first preview for theme %s: %s", a.Slug, a.Previews[0].ImageURL)
|
||||
screenshots = []string{a.Previews[0].ImageURL}
|
||||
} else {
|
||||
log.Printf("No preview images available for theme %s", a.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
// Release notes
|
||||
releaseNotes := ""
|
||||
if rn, ok := a.CurrentVersion.ReleaseNotes["en-US"]; ok && rn != "" {
|
||||
releaseNotes = rn
|
||||
} else {
|
||||
for _, rn := range a.CurrentVersion.ReleaseNotes {
|
||||
releaseNotes = rn
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
description := ""
|
||||
if d, ok := a.Summary["en-US"]; ok && d != "" {
|
||||
description = d
|
||||
} else {
|
||||
for _, d := range a.Summary {
|
||||
description = d
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
entries = append(entries, AppIndexEntry{
|
||||
Name: addonName,
|
||||
Version: a.CurrentVersion.Version,
|
||||
Type: finalType,
|
||||
DownloadURL: downloadURL,
|
||||
Maintainer: a.Author.Name,
|
||||
Icon: a.IconURL,
|
||||
Screenshots: screenshots,
|
||||
Tags: a.Tags,
|
||||
Description: description,
|
||||
URL: a.URL,
|
||||
License: a.License.Name,
|
||||
Notes: releaseNotes,
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("AMO results: %d", len(entries))
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// Search in local SPM index by type
|
||||
func searchLocalByType(typeFilter string) ([]AppIndexEntry, error) {
|
||||
allEntries, err := GetIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var filtered []AppIndexEntry
|
||||
for _, e := range allEntries {
|
||||
if strings.EqualFold(e.Type, typeFilter) {
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// Search in local SPM index for all, optionally filtered by name substring.
|
||||
func searchLocalAll(query string) ([]AppIndexEntry, error) {
|
||||
allEntries, err := GetIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if query == "" {
|
||||
return allEntries, nil
|
||||
}
|
||||
var result []AppIndexEntry
|
||||
for _, e := range allEntries {
|
||||
if strings.Contains(strings.ToLower(e.Name), strings.ToLower(query)) {
|
||||
result = append(result, e)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SearchPackages coordinates between local and AMO-based searches depending on filter
|
||||
func SearchPackages(query, filter string) ([]AppIndexEntry, error) {
|
||||
switch filter {
|
||||
case "addon":
|
||||
return searchMozillaAddons(query, "extension")
|
||||
case "theme":
|
||||
return searchMozillaAddons(query, "statictheme")
|
||||
case "layout", "bundle", "config":
|
||||
return searchLocalByType(filter)
|
||||
case "", "all":
|
||||
localAll, err := searchLocalAll(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("local search error: %w", err)
|
||||
}
|
||||
mozExt, err := searchMozillaAddons(query, "extension")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mozilla (extension) error: %w", err)
|
||||
}
|
||||
mozTheme, err := searchMozillaAddons(query, "statictheme")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mozilla (theme) error: %w", err)
|
||||
}
|
||||
return append(append(localAll, mozExt...), mozTheme...), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unrecognized filter: %s", filter)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue