package wallpaper import ( "context" "fmt" "strings" "time" "service.xpcool.com/internal/model/dto" ) // BingProvider 微软必应「每日一图」。 // // 特点:无需 API Key、每天只换一张、画质高(官方提供到 UHD)。 // 接口是公开的 HPImageArchive,返回最近若干天的图,天然带版权信息。 // // 局限:只有横版(1920x1080 / UHD),所以筛选竖版时返回空 —— 这是事实, // 不做「用别的图凑数」这种欺骗性的降级。 type BingProvider struct{} // bingBase 图片 CDN 前缀(接口返回的是相对路径)。 const bingBase = "https://www.bing.com" func (p *BingProvider) Code() string { return "bing" } func (p *BingProvider) Name() string { return "Bing 每日一图" } func (p *BingProvider) RequiresKey() bool { return false } func (p *BingProvider) List(ctx context.Context, cfg dto.WallpaperSourceConfig, q dto.WallpaperQuery) ([]dto.WallpaperItem, int, error) { // 本平台一页固定 8 张,size 参数无意义,故用 _ 接住 page, _ := normalizePage(q.Page, q.Size) // HPImageArchive 的 idx 是「从今天往前推几天」,n 最多 8。 // 故一页 8 张,最多取两页(16 天),再往前没有意义。 if page > 2 { return []dto.WallpaperItem{}, 0, nil } idx := (page - 1) * 8 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 } } base := baseOf(cfg.BaseUrl, bingBase) url := fmt.Sprintf("%s/HPImageArchive.aspx?format=js&idx=%d&n=8&mkt=zh-CN", base, idx) j, err := httpGetJSON(ctx, url, nil, 10*time.Second) if err != nil { return nil, 0, fmt.Errorf("获取 Bing 每日一图失败: %w", err) } var out []dto.WallpaperItem for _, img := range j.GetJsons("images") { urlbase := img.Get("urlbase").String() if urlbase == "" { // 少数情况下只给完整 url,退而求其次从 url 里截掉尺寸后缀 raw := img.Get("url").String() if raw == "" { continue } urlbase = strings.SplitN(raw, "_", 2)[0] } title, author := splitBingCopyright(img.Get("copyright").String()) start := img.Get("startdate").String() if t := img.Get("title").String(); t != "" { title = t } if title == "" { title = "Bing 每日一图 " + start } // Bing 的图固定 1920x1080 比例(UHD 同比例),故朝向直接判定为横版。 if !matchOrientation(16, 9, q.Orientation) { continue } out = append(out, dto.WallpaperItem{ Id: firstNonEmpty(img.Get("hsh").String(), start), Title: title, Width: 1920, Height: 1080, Orientation: 1, ThumbUrl: base + urlbase + "_400x240.jpg", PreviewUrl: base + urlbase + "_1920x1080.jpg", FullUrl: base + urlbase + "_UHD.jpg", Source: p.Code(), SourceName: p.Name(), FromOpen: true, Author: author, PageUrl: img.Get("copyrightlink").String(), License: "Bing 每日一图(版权归原作者)", }) } _ = openCache.Set(ctx, key, out, cacheTTLDaily) return out, len(out), nil } // splitBingCopyright 把 Bing 的版权串拆成「地点」与「作者」。 // // 原始格式形如:`某地风光 (© 张三/Getty Images)`。 // 括号可能出现在地名里,所以从**最后一个** "(©" 开始切。 func splitBingCopyright(s string) (title, author string) { s = strings.TrimSpace(s) if s == "" { return "", "" } i := strings.LastIndex(s, "(©") if i < 0 { return s, "" } title = strings.TrimSpace(s[:i]) author = strings.TrimSpace(s[i+len("(©"):]) author = strings.TrimSuffix(author, ")") return strings.TrimSpace(title), strings.TrimSpace(author) } // firstNonEmpty 返回第一个非空字符串(用于给可选字段挑兜底值)。 func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" }