package wallpaper import ( "context" "fmt" "net/url" "time" "github.com/gogf/gf/v2/encoding/gjson" "service.xpcool.com/internal/model/dto" ) // PexelsProvider Pexels 官方 API。 // // 特点:免费额度高、图片量大、接口直接给出多档尺寸且支持朝向筛选。 // 鉴权:Authorization 头直接放 API Key(注意与 Unsplash 不同,**没有** Client-ID 前缀)。 // 许可:可免费用于个人与商业用途,无需转存,直接用其 CDN 地址即可。 type PexelsProvider struct{} const pexelsBase = "https://api.pexels.com/v1" func (p *PexelsProvider) Code() string { return "pexels" } func (p *PexelsProvider) Name() string { return "Pexels" } func (p *PexelsProvider) RequiresKey() bool { return true } func (p *PexelsProvider) List(ctx context.Context, cfg dto.WallpaperSourceConfig, q dto.WallpaperQuery) ([]dto.WallpaperItem, int, error) { if cfg.ApiKey == "" { return nil, 0, fmt.Errorf("Pexels 未配置 API Key") } page, size := normalizePage(q.Page, q.Size) key := cacheKey(p.Code(), q) if v, err := openCache.Get(ctx, key); err == nil && !v.IsNil() { if cached, ok := v.Val().([]dto.WallpaperItem); ok { return cached, len(cached), nil } } // 没有关键词时走「精选」端点,避免请求空 query 的搜索接口 base := baseOf(cfg.BaseUrl, pexelsBase) var u string if keyword := firstNonEmpty(q.Query, cfg.DefaultQuery); keyword != "" { u = fmt.Sprintf("%s/search?query=%s&page=%d&per_page=%d", base, url.QueryEscape(keyword), page, size) } else { u = fmt.Sprintf("%s/curated?page=%d&per_page=%d", base, page, size) } if o := pexelsOrientation(q.Orientation); o != "" { u += "&orientation=" + url.QueryEscape(o) } headers := map[string]string{"Authorization": cfg.ApiKey} j, err := httpGetJSON(ctx, u, headers, 12*time.Second) if err != nil { return nil, 0, fmt.Errorf("获取 Pexels 列表失败: %w", err) } out := make([]dto.WallpaperItem, 0, size) for _, raw := range j.Get("photos").Array() { item := gjson.New(raw) id := item.Get("id").String() if id == "" { continue } w := item.Get("width").Int() h := item.Get("height").Int() if !matchOrientation(w, h, q.Orientation) { continue } out = append(out, dto.WallpaperItem{ Id: id, Title: firstNonEmpty(item.Get("alt").String(), "Pexels "+id), Width: w, Height: h, Orientation: orientationOf(w, h), ThumbUrl: item.Get("src.medium").String(), PreviewUrl: firstNonEmpty(item.Get("src.large2x").String(), item.Get("src.large").String()), FullUrl: item.Get("src.original").String(), Source: p.Code(), SourceName: p.Name(), FromOpen: true, Author: item.Get("photographer").String(), AuthorUrl: item.Get("photographer_url").String(), PageUrl: item.Get("url").String(), License: "Pexels License", }) } _ = openCache.Set(ctx, key, out, cacheTTLSearch) return out, j.Get("total_results").Int(), nil } // pexelsOrientation 把内部朝向编码翻译成 Pexels 的取值。 // 注意 Pexels 用的是 square(不是 Unsplash 的 squarish)。 func pexelsOrientation(o int) string { switch o { case 1: return "landscape" case 2: return "portrait" case 3: return "square" default: return "" } }