package main import ( "encoding/json" "fmt" "io" "log" "os" "path/filepath" "sort" "strings" "sync" "time" ) 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 []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 { Date string `json:"date"` URLBase string `json:"urlbase"` 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"` } var bingMu sync.Mutex func bingDir() string { return filepath.Join(configDir(), "bing") } func bingHistoryPath() string { return filepath.Join(configDir(), "bing_history.json") } func loadBingHistory() *BingHistory { data, err := os.ReadFile(bingHistoryPath()) if err != nil { return &BingHistory{} } var h BingHistory if json.Unmarshal(data, &h) != nil { return &BingHistory{} } return &h } func saveBingHistory(h *BingHistory) error { 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[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 } } cfg := loadConfig() markets := cfg.BingMarkets if len(markets) == 0 { markets = bingMarkets } // 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 } 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 } 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 := fmt.Sprintf("%s_%s.jpg", market, date) localPath := filepath.Join(bingDir(), filename) os.WriteFile(localPath, imgData, 0644) log.Printf("Bing biturl 壁纸已下载: %s (%d bytes)", filename, len(imgData)) newRecords[key] = BingRecord{ Date: date, URLBase: bit.URL, Copyright: bit.Copyright, Filename: filename, Market: market, Source: "bing_biturl", } time.Sleep(300 * time.Millisecond) } } // 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 allMap { records = append(records, r) } sort.Slice(records, func(i, j int) bool { 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, } if existing.CurrentIdx < len(records) { history.CurrentIdx = existing.CurrentIdx } saveBingHistory(history) log.Printf("Bing 壁纸: 共 %d 张 (新增 %d 张)", len(records), len(newRecords)) reloadWallpaper() } func getCurrentBingPath() string { h := loadBingHistory() if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) { return "" } 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 { h := loadBingHistory() if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) { return nil } return &h.Records[h.CurrentIdx] } // bingPrev moves to the previous (older) image, skipping hidden ones. func bingPrev() { bingMu.Lock() defer bingMu.Unlock() h := loadBingHistory() if len(h.Records) == 0 { return } 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() h := loadBingHistory() if len(h.Records) == 0 { return } 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() } func bingToggleFavorite() string { bingMu.Lock() defer bingMu.Unlock() h := loadBingHistory() if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) { return "" } r := &h.Records[h.CurrentIdx] r.Favorited = !r.Favorited saveBingHistory(h) if r.Favorited { log.Printf("Bing 壁纸: 已收藏 (date=%s)", r.Date) return "☆ 取消收藏" } log.Printf("Bing 壁纸: 已取消收藏 (date=%s)", r.Date) 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,"hidden":false}` } r := h.Records[h.CurrentIdx] label := "☆" if r.Favorited { label = "⭐" } data, _ := json.Marshal(map[string]interface{}{ "fav": r.Favorited, "label": label, "copyright": r.Copyright, "date": r.Date, "filename": r.Filename, "idx": h.CurrentIdx, "total": len(h.Records), "hidden": r.Hidden, "market": r.Market, }) return string(data) } func bingFavoritesJSON() string { h := loadBingHistory() type favItem struct { Date string `json:"date"` Copyright string `json:"copyright"` Filename string `json:"filename"` Idx int `json:"idx"` } var favs []favItem for i, r := range h.Records { if r.Favorited && !r.Hidden { favs = append(favs, favItem{r.Date, r.Copyright, r.Filename, i}) } } if favs == nil { favs = []favItem{} } data, _ := json.Marshal(favs) return string(data) } func bingSetByIdx(idx int) string { bingMu.Lock() defer bingMu.Unlock() h := loadBingHistory() if idx < 0 || idx >= len(h.Records) { return bingCurrentState() } h.CurrentIdx = idx saveBingHistory(h) bingReloadImage() return bingCurrentState() } 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) } func bingWallpaperLoop() { cfg := loadConfig() if cfg.WallpaperType == WPBing { h := loadBingHistory() if len(h.Records) == 0 { fetchBingHistory() } else { reloadWallpaper() } } // Auto-refresh: cycle through images (newest direction) go func() { ticker := time.NewTicker(1 * time.Hour) for range ticker.C { cfg := loadConfig() if cfg.WallpaperType != WPBing || !cfg.BingAutoRefresh { continue } bingMu.Lock() h := loadBingHistory() if len(h.Records) <= 1 { bingMu.Unlock() continue } // 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() log.Printf("Bing 自动切换: idx=%d/%d", h.CurrentIdx, len(h.Records)) } }() // Periodic fetch go func() { fetchTicker := time.NewTicker(4 * time.Hour) for range fetchTicker.C { cfg := loadConfig() if cfg.WallpaperType == WPBing { go fetchBingHistory() } } }() }