Files
MeBox/internal/service/download_polling.go
T
2026-06-26 08:42:16 +08:00

202 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"time"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/model"
)
const completedTorrentOrganizeQueueSize = 64
var completedTorrentOrganizeCooldown = 3 * time.Second
// poll fans out qBittorrent /torrents/info every 5 s as WS events. The
// payload is opaque to the client; the React store merges by hash.
func (d *DownloadService) poll(ctx context.Context) {
t := time.NewTicker(5 * time.Second)
defer t.Stop()
// prevStates tracks previous completion states to detect "just finished"
if d.prevStates == nil {
d.prevStates = make(map[string]bool)
}
for {
select {
case <-ctx.Done():
return
case <-d.stopCh:
return
case <-t.C:
}
live, err := d.qb.List(ctx, "")
if err != nil {
continue
}
rows, _ := d.repo.Download.List(ctx)
taskByKey := tasksByTorrentIdentity(rows)
d.processDownloadSnapshot(ctx, live, taskByKey)
d.hub.Publish("download", map[string]any{"torrents": live})
}
}
func (d *DownloadService) processDownloadSnapshot(ctx context.Context, live []QBitTorrent, taskByKey map[string]model.DownloadTask) {
d.recordLiveTorrentSnapshot(live)
firstSnapshot := d.beginDownloadSnapshot()
for _, torrent := range live {
d.processTorrentSnapshot(ctx, torrent, taskByKey, firstSnapshot)
}
}
func (d *DownloadService) beginDownloadSnapshot() bool {
d.mu.Lock()
defer d.mu.Unlock()
if d.prevStates == nil {
d.prevStates = make(map[string]bool)
}
firstSnapshot := !d.pollInitialized
if firstSnapshot {
d.pollInitialized = true
}
return firstSnapshot
}
func (d *DownloadService) processTorrentSnapshot(ctx context.Context, torrent QBitTorrent, taskByKey map[string]model.DownloadTask, firstSnapshot bool) {
stateKey := completedTorrentQueueKey(torrent)
taskNeedsOrganize := d.downloadSnapshotTaskNeedsOrganize(ctx, torrent, taskByKey)
d.syncDownloadTaskProgress(ctx, torrent, taskByKey)
if stateKey == "" {
return
}
if d.completedTorrentShouldQueue(stateKey, qbitTorrentCompleted(torrent), firstSnapshot, taskNeedsOrganize) &&
d.enqueueCompletedTorrent(torrent) {
d.markCompletedTorrentState(stateKey)
}
}
func (d *DownloadService) downloadSnapshotTaskNeedsOrganize(ctx context.Context, torrent QBitTorrent, taskByKey map[string]model.DownloadTask) bool {
matchedTask, hasTask := findMatchingTaskByTorrentIdentity(torrent.Name, taskByKey)
if !hasTask || d.completedTorrentCatchupRecorded(ctx, torrent) || !d.downloadAutoOrganizeEnabled(ctx) {
return false
}
return downloadTaskNeedsCompletion(matchedTask) || recentlyCompletedTorrent(torrent, time.Now())
}
func (d *DownloadService) completedTorrentShouldQueue(stateKey string, complete, firstSnapshot, taskNeedsOrganize bool) bool {
d.mu.Lock()
defer d.mu.Unlock()
if d.prevStates == nil {
d.prevStates = make(map[string]bool)
}
wasComplete, wasSeen := d.prevStates[stateKey]
switch {
case complete && (firstSnapshot || !wasSeen):
// 首次快照里已完成的种子:此前一律标记「已见过」并跳过整理,
// 导致「下载完成时应用恰好不在线/正在重启」的种子永远不会被
// 自动整理入库。现在对最近完成的种子补一次整理
// (onTorrentComplete 内部仍受 organize.auto 开关约束,且
// 整理对已存在的目标文件幂等跳过)。
d.prevStates[stateKey] = true
return taskNeedsOrganize
case complete && !wasComplete:
return true
case complete && taskNeedsOrganize:
return true
case complete:
d.prevStates[stateKey] = true
default:
d.prevStates[stateKey] = false
}
return false
}
func (d *DownloadService) markCompletedTorrentState(stateKey string) {
d.mu.Lock()
d.prevStates[stateKey] = true
d.mu.Unlock()
}
func (d *DownloadService) startAutoOrganizeWorker(ctx context.Context) {
d.mu.Lock()
if d.organizeQueue == nil {
d.organizeQueue = make(chan QBitTorrent, completedTorrentOrganizeQueueSize)
}
if d.organizeQueued == nil {
d.organizeQueued = make(map[string]struct{})
}
d.mu.Unlock()
d.organizeOnce.Do(func() {
go d.autoOrganizeWorker(ctx)
})
}
func (d *DownloadService) enqueueCompletedTorrent(torrent QBitTorrent) bool {
key := completedTorrentQueueKey(torrent)
if key == "" {
return false
}
d.mu.Lock()
if d.organizeQueue == nil {
d.organizeQueue = make(chan QBitTorrent, completedTorrentOrganizeQueueSize)
}
if d.organizeQueued == nil {
d.organizeQueued = make(map[string]struct{})
}
if _, ok := d.organizeQueued[key]; ok {
d.mu.Unlock()
return true
}
select {
case d.organizeQueue <- torrent:
d.organizeQueued[key] = struct{}{}
d.mu.Unlock()
return true
default:
d.mu.Unlock()
if d.log != nil {
d.log.Warn("auto organize queue full; will retry completed torrent later",
zap.String("hash", torrent.Hash),
zap.String("name", torrent.Name))
}
return false
}
}
func (d *DownloadService) autoOrganizeWorker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-d.stopCh:
return
case torrent := <-d.organizeQueue:
d.onTorrentComplete(ctx, torrent)
d.markCompletedTorrentOrganizeDone(torrent)
if completedTorrentOrganizeCooldown <= 0 {
continue
}
timer := time.NewTimer(completedTorrentOrganizeCooldown)
select {
case <-ctx.Done():
timer.Stop()
return
case <-d.stopCh:
timer.Stop()
return
case <-timer.C:
}
}
}
}
func (d *DownloadService) markCompletedTorrentOrganizeDone(torrent QBitTorrent) {
key := completedTorrentQueueKey(torrent)
if key == "" {
return
}
d.mu.Lock()
delete(d.organizeQueued, key)
d.mu.Unlock()
}