mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-10 23:36:37 +08:00
refactor: split modules and harden scraping workflows
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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, torrent.Progress >= 1.0, 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()
|
||||
}
|
||||
Reference in New Issue
Block a user