新增: Bing 壁纸多市场获取/2000张历史/第三方图源/dislike/重下/方向修复

This commit is contained in:
lxy
2026-10-01 01:02:41 +08:00
parent bbbc32142d
commit 2036c9c89a
8 changed files with 431 additions and 68 deletions
+344 -55
View File
@@ -13,15 +13,26 @@ import (
"time"
)
const bingAPIBase = "https://www.bing.com/HPImageArchive.aspx?format=js&n=8&mkt=zh-CN"
const bingAPIPrefix = "https://www.bing.com/HPImageArchive.aspx?format=js&n=8"
type bingImage struct {
StartDate string `json:"startdate"`
URL string `json:"url"`
URLBase string `json:"urlbase"`
Copyright string `json:"copyright"`
}
type bingResponse struct {
Images []struct {
StartDate string `json:"startdate"`
URL string `json:"url"`
URLBase string `json:"urlbase"`
Copyright string `json:"copyright"`
} `json:"images"`
Images []bingImage `json:"images"`
}
type biturlResponse struct {
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
URL string `json:"url"`
Copyright string `json:"copyright"`
CopyrightLink string `json:"copyright_link"`
Resolution string `json:"resolution"`
}
type BingRecord struct {
@@ -30,8 +41,33 @@ type BingRecord struct {
Copyright string `json:"copyright"`
Filename string `json:"filename"`
Favorited bool `json:"favorited"`
Hidden bool `json:"hidden"`
Market string `json:"market"`
Source string `json:"source"`
}
const maxBingRecords = 2000
var bingMarkets = []string{
"zh-CN", "en-US", "ja-JP", "de-DE", "fr-FR",
"en-GB", "ko-KR", "ru-RU", "pt-BR", "es-ES",
}
var bingMarketLabels = map[string]string{
"zh-CN": "中国",
"en-US": "美国",
"ja-JP": "日本",
"de-DE": "德国",
"fr-FR": "法国",
"en-GB": "英国",
"ko-KR": "韩国",
"ru-RU": "俄罗斯",
"pt-BR": "巴西",
"es-ES": "西班牙",
}
const bingBiturlAPI = "https://bing.biturl.top/?format=json"
type BingHistory struct {
Records []BingRecord `json:"records"`
CurrentIdx int `json:"currentIdx"`
@@ -60,91 +96,249 @@ func loadBingHistory() *BingHistory {
}
func saveBingHistory(h *BingHistory) error {
data, _ := json.MarshalIndent(h, "", " ")
data, err := json.MarshalIndent(h, "", " ")
if err != nil {
log.Printf("Bing 历史序列化失败: %v", err)
return err
}
return os.WriteFile(bingHistoryPath(), data, 0644)
}
// bingKey builds a composite dedup key: market + "_" + date
func bingKey(market, date string) string {
if market == "" {
return "_" + date
}
return market + "_" + date
}
// downloadBingImage downloads a single Bing image from the record's URLBase
func downloadBingImage(r *BingRecord) error {
imgURL := r.URLBase
if imgURL == "" {
return fmt.Errorf("empty URLBase for %s", r.Filename)
}
if !strings.HasPrefix(imgURL, "http") {
imgURL = "https://www.bing.com" + imgURL
}
// For official Bing source, ensure resolution suffix
if r.Source == "bing_official" || r.Source == "" {
if !strings.Contains(imgURL, "_1920x1080") && !strings.HasSuffix(imgURL, ".jpg") {
imgURL += "_1920x1080.jpg"
}
}
imgResp, err := httpClient.Get(imgURL)
if err != nil {
return fmt.Errorf("http get failed: %w", err)
}
defer imgResp.Body.Close()
imgData, err := io.ReadAll(imgResp.Body)
if err != nil {
return fmt.Errorf("read body failed: %w", err)
}
if len(imgData) == 0 {
return fmt.Errorf("empty response body")
}
localPath := filepath.Join(bingDir(), r.Filename)
return os.WriteFile(localPath, imgData, 0644)
}
// fetchBingHistory fetches Bing wallpapers from multiple markets and sources.
// Phase 1: Official Bing API per market (idx=0,8,...192)
// Phase 2: biturl.top supplement per market (5 attempts each)
// Phase 3: Enforce maxBingRecords cap
func fetchBingHistory() {
bingMu.Lock()
defer bingMu.Unlock()
os.MkdirAll(bingDir(), 0755)
existing := loadBingHistory()
// Build composite-key map from existing records
existingMap := make(map[string]BingRecord)
for _, r := range existing.Records {
existingMap[r.Date] = r
existingMap[bingKey(r.Market, r.Date)] = r
// Migration: old records with Market="" were from zh-CN single market
if r.Market == "" {
existingMap[bingKey("zh-CN", r.Date)] = r
}
}
// 分页下载: idx=0,8,16... 直到命中已有记录或无新图片
for idx := 0; idx < 80; idx += 8 {
url := fmt.Sprintf("%s&idx=%d", bingAPIBase, idx)
resp, err := httpClient.Get(url)
if err != nil {
log.Printf("Bing API 请求失败 (idx=%d): %v", idx, err)
break
}
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var br bingResponse
if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 {
break
}
cfg := loadConfig()
markets := cfg.BingMarkets
if len(markets) == 0 {
markets = bingMarkets
}
newCount := 0
for _, img := range br.Images {
date := img.StartDate
if date == "" {
// track new records per key for dedup across phases
newRecords := make(map[string]BingRecord)
// ---- Phase 1: Official Bing API per market ----
for _, market := range markets {
for idx := 0; idx < 192; idx += 8 {
url := fmt.Sprintf("%s&mkt=%s&idx=%d", bingAPIPrefix, market, idx)
resp, err := httpClient.Get(url)
if err != nil {
log.Printf("Bing API 请求失败 (market=%s, idx=%d): %v", market, idx, err)
break
}
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var br bingResponse
if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 {
break
}
pageNew := 0
for _, img := range br.Images {
date := img.StartDate
if date == "" {
continue
}
key := bingKey(market, date)
if _, exists := existingMap[key]; exists {
continue
}
if _, exists := newRecords[key]; exists {
continue
}
// Download the image
imgURL := img.URL
if !strings.HasPrefix(imgURL, "http") {
imgURL = "https://www.bing.com" + imgURL
}
imgResp, err := httpClient.Get(imgURL)
if err != nil {
continue
}
imgData, _ := io.ReadAll(imgResp.Body)
imgResp.Body.Close()
if len(imgData) == 0 {
continue
}
filename := fmt.Sprintf("%s_%s.jpg", market, date)
localPath := filepath.Join(bingDir(), filename)
os.WriteFile(localPath, imgData, 0644)
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
newRecords[key] = BingRecord{
Date: date,
URLBase: img.URLBase,
Copyright: img.Copyright,
Filename: filename,
Market: market,
Source: "bing_official",
}
pageNew++
}
if pageNew == 0 && idx > 0 {
break
}
time.Sleep(500 * time.Millisecond)
}
}
// ---- Phase 2: biturl.top supplement ----
for _, market := range markets {
for attempt := 0; attempt < 5; attempt++ {
url := fmt.Sprintf("%s&mkt=%s", bingBiturlAPI, market)
resp, err := httpClient.Get(url)
if err != nil {
log.Printf("Bing biturl 请求失败 (market=%s): %v", market, err)
time.Sleep(300 * time.Millisecond)
continue
}
if _, exists := existingMap[date]; exists {
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var bit biturlResponse
if json.Unmarshal(data, &bit) != nil || bit.StartDate == "" {
time.Sleep(300 * time.Millisecond)
continue
}
imgURL := img.URL
if !strings.HasPrefix(imgURL, "http") {
imgURL = "https://www.bing.com" + imgURL
date := bit.StartDate
key := bingKey(market, date)
if _, exists := existingMap[key]; exists {
time.Sleep(300 * time.Millisecond)
continue
}
if _, exists := newRecords[key]; exists {
time.Sleep(300 * time.Millisecond)
continue
}
// Download from biturl URL
imgURL := bit.URL
imgResp, err := httpClient.Get(imgURL)
if err != nil {
time.Sleep(300 * time.Millisecond)
continue
}
imgData, _ := io.ReadAll(imgResp.Body)
imgResp.Body.Close()
if len(imgData) == 0 {
time.Sleep(300 * time.Millisecond)
continue
}
filename := date + ".jpg"
filename := fmt.Sprintf("%s_%s.jpg", market, date)
localPath := filepath.Join(bingDir(), filename)
os.WriteFile(localPath, imgData, 0644)
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
log.Printf("Bing biturl 壁纸已下载: %s (%d bytes)", filename, len(imgData))
existingMap[date] = BingRecord{
newRecords[key] = BingRecord{
Date: date,
URLBase: img.URLBase,
Copyright: img.Copyright,
URLBase: bit.URL,
Copyright: bit.Copyright,
Filename: filename,
Market: market,
Source: "bing_biturl",
}
newCount++
time.Sleep(300 * time.Millisecond)
}
if newCount == 0 && idx > 0 {
break
}
time.Sleep(500 * time.Millisecond)
}
// 按 API 返回顺序重建 records (newest first)
// 用 existingMap 里所有记录按 date 降序排列
// Merge existing + new records
allMap := make(map[string]BingRecord)
for k, r := range existingMap {
allMap[k] = r
}
for k, r := range newRecords {
allMap[k] = r
}
// Sort by date descending, then by market for determinism
var records []BingRecord
for _, r := range existingMap {
for _, r := range allMap {
records = append(records, r)
}
sort.Slice(records, func(i, j int) bool {
return records[i].Date > records[j].Date
if records[i].Date != records[j].Date {
return records[i].Date > records[j].Date
}
return records[i].Market < records[j].Market
})
// ---- Phase 3: Enforce 2000 cap ----
if len(records) > maxBingRecords {
// Remove oldest records and their files
toRemove := records[maxBingRecords:]
records = records[:maxBingRecords]
for _, r := range toRemove {
p := filepath.Join(bingDir(), r.Filename)
os.Remove(p)
}
log.Printf("Bing 壁纸: 裁剪到 %d 张, 删除 %d 张旧图", maxBingRecords, len(toRemove))
}
history := &BingHistory{
Records: records,
CurrentIdx: 0,
@@ -154,7 +348,7 @@ func fetchBingHistory() {
}
saveBingHistory(history)
log.Printf("Bing 壁纸: 共 %d 张", len(records))
log.Printf("Bing 壁纸: 共 %d 张 (新增 %d 张)", len(records), len(newRecords))
reloadWallpaper()
}
@@ -163,7 +357,18 @@ func getCurrentBingPath() string {
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
return ""
}
return filepath.Join(bingDir(), h.Records[h.CurrentIdx].Filename)
r := &h.Records[h.CurrentIdx]
p := filepath.Join(bingDir(), r.Filename)
if _, err := os.Stat(p); err == nil {
return p
}
// File missing, try re-download
log.Printf("Bing 壁纸文件缺失, 尝试重新下载: %s", r.Filename)
if err := downloadBingImage(r); err != nil {
log.Printf("Bing 重新下载失败: %s, %v", r.Filename, err)
return ""
}
return p
}
func getCurrentBingRecord() *BingRecord {
@@ -174,6 +379,7 @@ func getCurrentBingRecord() *BingRecord {
return &h.Records[h.CurrentIdx]
}
// bingPrev moves to the previous (older) image, skipping hidden ones.
func bingPrev() {
bingMu.Lock()
defer bingMu.Unlock()
@@ -182,12 +388,23 @@ func bingPrev() {
if len(h.Records) == 0 {
return
}
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
attempts := 0
for {
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
if !h.Records[h.CurrentIdx].Hidden {
break
}
attempts++
if attempts >= len(h.Records) {
return
}
}
saveBingHistory(h)
log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
bingReloadImage()
}
// bingNext moves to the next (newer) image, skipping hidden ones.
func bingNext() {
bingMu.Lock()
defer bingMu.Unlock()
@@ -196,7 +413,17 @@ func bingNext() {
if len(h.Records) == 0 {
return
}
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
attempts := 0
for {
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
if !h.Records[h.CurrentIdx].Hidden {
break
}
attempts++
if attempts >= len(h.Records) {
return
}
}
saveBingHistory(h)
log.Printf("Bing 壁纸: 下一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
bingReloadImage()
@@ -221,10 +448,46 @@ func bingToggleFavorite() string {
return "★ 收藏当前壁纸"
}
// bingToggleDislike toggles the hidden flag on the current record.
// When hidden, auto-advances to the next visible image.
func bingToggleDislike() string {
bingMu.Lock()
defer bingMu.Unlock()
h := loadBingHistory()
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
return bingCurrentState()
}
r := &h.Records[h.CurrentIdx]
r.Hidden = !r.Hidden
saveBingHistory(h)
if r.Hidden {
log.Printf("Bing 壁纸: 已隐藏 (date=%s)", r.Date)
// Advance to next visible
attempts := 0
for {
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
if !h.Records[h.CurrentIdx].Hidden {
break
}
attempts++
if attempts >= len(h.Records) {
return bingCurrentState()
}
}
saveBingHistory(h)
bingReloadImage()
return bingCurrentState()
}
log.Printf("Bing 壁纸: 已取消隐藏 (date=%s)", r.Date)
return bingCurrentState()
}
func bingCurrentState() string {
h := loadBingHistory()
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
return `{"fav":false,"label":"☆","copyright":"","idx":0,"total":0}`
return `{"fav":false,"label":"☆","copyright":"","idx":0,"total":0,"hidden":false}`
}
r := h.Records[h.CurrentIdx]
label := "☆"
@@ -239,6 +502,8 @@ func bingCurrentState() string {
"filename": r.Filename,
"idx": h.CurrentIdx,
"total": len(h.Records),
"hidden": r.Hidden,
"market": r.Market,
})
return string(data)
}
@@ -253,7 +518,7 @@ func bingFavoritesJSON() string {
}
var favs []favItem
for i, r := range h.Records {
if r.Favorited {
if r.Favorited && !r.Hidden {
favs = append(favs, favItem{r.Date, r.Copyright, r.Filename, i})
}
}
@@ -279,6 +544,18 @@ func bingSetByIdx(idx int) string {
func bingThumbDataURI(filename string) string {
p := filepath.Join(bingDir(), filename)
// Auto redownload if missing
if _, err := os.Stat(p); err != nil {
// Try to find the record and re-download
h := loadBingHistory()
for _, r := range h.Records {
if r.Filename == filename {
log.Printf("Bing 缩略图文件缺失, 尝试重新下载: %s", filename)
downloadBingImage(&r)
break
}
}
}
return imageToDataURI(p)
}
@@ -293,7 +570,7 @@ func bingWallpaperLoop() {
}
}
// 定时切换壁纸 (1 小时间隔)
// Auto-refresh: cycle through images (newest direction)
go func() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
@@ -307,7 +584,18 @@ func bingWallpaperLoop() {
bingMu.Unlock()
continue
}
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
// Move to next (newer) image, skip hidden
attempts := 0
for {
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
if !h.Records[h.CurrentIdx].Hidden {
break
}
attempts++
if attempts >= len(h.Records) {
break
}
}
saveBingHistory(h)
bingMu.Unlock()
bingReloadImage()
@@ -315,6 +603,7 @@ func bingWallpaperLoop() {
}
}()
// Periodic fetch
go func() {
fetchTicker := time.NewTicker(4 * time.Hour)
for range fetchTicker.C {
@@ -324,4 +613,4 @@ func bingWallpaperLoop() {
}
}
}()
}
}