package wallpaper import ( "context" "fmt" "time" "github.com/gogf/gf/v2/encoding/gjson" "service.xpcool.com/internal/model/dto" ) // PicsumProvider Lorem Picsum 随机摄影图库。 // // 特点:无需 API Key、量极大、支持按 id 取任意尺寸。 // 图片本身来自 Unsplash,故作者信息与许可沿用 Unsplash。 // // 局限:接口不支持关键词搜索、也不支持朝向筛选,两者都只能取回后在本地过滤。 // 为了过滤后仍有足够条目,这里会一次多取一些(接口上限 100 条)。 type PicsumProvider struct{} const picsumBase = "https://picsum.photos" func (p *PicsumProvider) Code() string { return "picsum" } func (p *PicsumProvider) Name() string { return "Lorem Picsum" } func (p *PicsumProvider) RequiresKey() bool { return false } func (p *PicsumProvider) List(ctx context.Context, cfg dto.WallpaperSourceConfig, q dto.WallpaperQuery) ([]dto.WallpaperItem, int, error) { 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 } } // 需要本地过滤(搜索词 / 朝向)时必须多取:该接口的列表是按 id 顺序返回的, // 前几页几乎全是横图,只多取三四倍的话筛竖版经常一条都不剩(实测踩过)。 // 故只要带过滤条件就直接取满接口上限 100 条。 fetch := size if q.Query != "" || q.Orientation != 0 { fetch = 100 } base := baseOf(cfg.BaseUrl, picsumBase) url := fmt.Sprintf("%s/v2/list?page=%d&limit=%d", base, page, fetch) j, err := httpGetJSON(ctx, url, nil, 12*time.Second) if err != nil { return nil, 0, fmt.Errorf("获取 Picsum 列表失败: %w", err) } // 该接口的响应体是**根数组**(没有外层对象),故用 Array() 逐项包装成 gjson。 // 注意 gjson.Json 自身没有 IsArray(),判类型要走 Var().IsSlice()。 if !j.Var().IsSlice() { return nil, 0, fmt.Errorf("Picsum 响应格式异常:期望数组") } out := make([]dto.WallpaperItem, 0, size) for _, raw := range j.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 } author := item.Get("author").String() // Picsum 不提供标题,用「作者 (#id)」拼一个可辨识的标题 title := author if title == "" { title = "Picsum #" + id } else { title = fmt.Sprintf("%s (#%s)", author, id) } // 关键词只能本地匹配(作者名 / 标题),该平台没有搜索能力 if q.Query != "" && !containsFold(author, q.Query) && !containsFold(title, q.Query) { continue } if len(out) >= size { break } out = append(out, dto.WallpaperItem{ Id: id, Title: title, Width: w, Height: h, Orientation: orientationOf(w, h), // 按原图比例缩出目标宽度,避免被接口裁剪改变构图 ThumbUrl: picsumScaleURL(base, id, 480, w, h), PreviewUrl: picsumScaleURL(base, id, 1600, w, h), FullUrl: item.Get("download_url").String(), Source: p.Code(), SourceName: p.Name(), FromOpen: true, Author: author, AuthorUrl: item.Get("url").String(), PageUrl: item.Get("url").String(), License: "Unsplash License(来自 Lorem Picsum)", }) } _ = openCache.Set(ctx, key, out, cacheTTLSearch) return out, len(out), nil } // picsumScaleURL 生成 Picsum 的等比缩放地址。 // // Picsum 的 /id/{id}/{w}/{h} 会把图**裁剪**到给定尺寸, // 直接传 16:9 会切掉竖图的两头,所以这里按原图比例算出高度 —— 纯缩放、不裁剪。 func picsumScaleURL(base, id string, targetW, w, h int) string { if w <= 0 || h <= 0 { return fmt.Sprintf("%s/id/%s/%d/%d", base, id, targetW, targetW*9/16) } if targetW >= w { // 原图比目标还小就别放大了,直接用原尺寸 return fmt.Sprintf("%s/id/%s/%d/%d", base, id, w, h) } targetH := h * targetW / w return fmt.Sprintf("%s/id/%s/%d/%d", base, id, targetW, targetH) }