242 lines
6.3 KiB
Go
242 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|