package core // 推送历史:记住「哪些画廊被推送过」,与任务列表解耦 // // 为什么单独存一份: // // state.json 里的 tasks 是「当前要下载的东西」,用户点「清除已完成」就会删掉。 // 但「这个画廊我推送过」是个历史事实,删了任务不该忘掉 —— // 油猴脚本靠它把标题标灰,让你刷新后还认得出哪些推过。 // // 所以单独一个 pushed.json,放在和 state.json 同一个目录(%APPDATA%\EH Downloader\)。 // 只有 gid 是必须的,token/title 留着方便排查。 // // 清理方式:删掉 pushed.json 即可;或者在设置里提供「清空推送历史」。 import ( "encoding/json" "os" "path/filepath" "sort" "sync" "time" ) // PushedEntry 一条推送记录 type PushedEntry struct { GID string `json:"gid"` Token string `json:"token,omitempty"` Title string `json:"title,omitempty"` PushedAt int64 `json:"pushedAt"` Completed bool `json:"completed,omitempty"` // 曾经下载完成过 } // PushedState pushed.json 的顶层结构 type PushedState struct { Entries []PushedEntry `json:"entries"` } // PushedStore 推送历史存储 type PushedStore struct { mu sync.RWMutex path string state PushedState flushTimer *time.Timer } // NewPushedStore 打开(或新建)推送历史文件 func NewPushedStore(filePath string) *PushedStore { p := &PushedStore{path: filePath} p.load() return p } func (p *PushedStore) load() { p.mu.Lock() defer p.mu.Unlock() p.state = PushedState{Entries: []PushedEntry{}} raw, err := os.ReadFile(p.path) if err != nil { return } var loaded PushedState if json.Unmarshal(raw, &loaded) == nil { if loaded.Entries == nil { loaded.Entries = []PushedEntry{} } p.state = loaded } } // Flush 原子写 func (p *PushedStore) Flush() { p.mu.RLock() data, err := json.MarshalIndent(p.state, "", " ") path := p.path p.mu.RUnlock() if err != nil { return } tmp := path + ".tmp" if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return } if err := os.WriteFile(tmp, data, 0o644); err != nil { return } _ = os.Rename(tmp, path) } // ScheduleFlush 合并短时间内的多次写 func (p *PushedStore) ScheduleFlush(delay int) { if delay <= 0 { delay = 400 } p.mu.Lock() defer p.mu.Unlock() if p.flushTimer != nil { return } p.flushTimer = time.AfterFunc(time.Duration(delay)*time.Millisecond, func() { p.mu.Lock() p.flushTimer = nil p.mu.Unlock() p.Flush() }) } // Stop 停掉待执行的延迟写 func (p *PushedStore) Stop() { p.mu.Lock() if p.flushTimer != nil { p.flushTimer.Stop() p.flushTimer = nil } p.mu.Unlock() } // Mark 记一笔推送(同 gid 重复推送只更新时间,不重复添加) func (p *PushedStore) Mark(gid, token, title string) { gid = trim(gid) if gid == "" { return } p.mu.Lock() for i := range p.state.Entries { if p.state.Entries[i].GID == gid { if token != "" { p.state.Entries[i].Token = token } if title != "" { p.state.Entries[i].Title = title } p.state.Entries[i].PushedAt = time.Now().Unix() p.mu.Unlock() p.ScheduleFlush(0) return } } p.state.Entries = append(p.state.Entries, PushedEntry{ GID: gid, Token: token, Title: title, PushedAt: time.Now().Unix(), }) p.mu.Unlock() p.ScheduleFlush(0) } // MarkBatch 批量记 func (p *PushedStore) MarkBatch(items []PushedEntry) { if len(items) == 0 { return } p.mu.Lock() now := time.Now().Unix() idx := map[string]int{} for i := range p.state.Entries { idx[p.state.Entries[i].GID] = i } for _, it := range items { gid := trim(it.GID) if gid == "" { continue } if i, ok := idx[gid]; ok { p.state.Entries[i].Token = it.Token p.state.Entries[i].Title = it.Title p.state.Entries[i].PushedAt = now continue } idx[gid] = len(p.state.Entries) p.state.Entries = append(p.state.Entries, PushedEntry{ GID: gid, Token: it.Token, Title: it.Title, PushedAt: now, }) } p.mu.Unlock() p.ScheduleFlush(0) } // MarkCompleted 标记某画廊已下载完成(只打标,不删记录) func (p *PushedStore) MarkCompleted(gid string) { gid = trim(gid) if gid == "" { return } p.mu.Lock() found := false for i := range p.state.Entries { if p.state.Entries[i].GID == gid { p.state.Entries[i].Completed = true found = true break } } p.mu.Unlock() if found { p.ScheduleFlush(0) } } // GIDs 取全部推送过的 gid func (p *PushedStore) GIDs() []string { p.mu.RLock() defer p.mu.RUnlock() out := make([]string, 0, len(p.state.Entries)) for _, e := range p.state.Entries { out = append(out, e.GID) } return out } // Entries 取全部记录(按推送时间倒序,新的在前) func (p *PushedStore) Entries() []PushedEntry { p.mu.RLock() defer p.mu.RUnlock() out := make([]PushedEntry, len(p.state.Entries)) copy(out, p.state.Entries) sort.SliceStable(out, func(i, j int) bool { return out[i].PushedAt > out[j].PushedAt }) return out } // Count 记录条数 func (p *PushedStore) Count() int { p.mu.RLock() defer p.mu.RUnlock() return len(p.state.Entries) } // Clear 清空推送历史 func (p *PushedStore) Clear() { p.mu.Lock() p.state = PushedState{Entries: []PushedEntry{}} p.mu.Unlock() p.ScheduleFlush(0) } // trim 去空白 func trim(s string) string { start, end := 0, len(s) for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n' || s[start] == '\r') { start++ } for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\n' || s[end-1] == '\r') { end-- } return s[start:end] }