mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
42ef19b7e5
Fixes #65
328 lines
9.6 KiB
Go
328 lines
9.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
)
|
|
|
|
// completedTorrentCatchupWindow 限定重启补整理只覆盖最近完成的种子,
|
|
// 防止每次启动都把全部历史种子重新过一遍整理流程。
|
|
const completedTorrentCatchupWindow = 24 * time.Hour
|
|
|
|
const completedTorrentCatchupSettingPrefix = "download.auto_organized."
|
|
const completedTorrentNotifySettingPrefix = "download.completed_notified."
|
|
|
|
func (d *DownloadService) downloadAutoOrganizeEnabled(ctx context.Context) bool {
|
|
if d == nil || d.repo == nil || d.repo.Setting == nil {
|
|
return false
|
|
}
|
|
if v, err := d.repo.Setting.Get(ctx, "organizer.auto_after_download"); err == nil && parseBoolSetting(v, false) {
|
|
return true
|
|
}
|
|
if v, err := d.repo.Setting.Get(ctx, "organize.auto"); err == nil && parseBoolSetting(v, false) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// recentlyCompletedTorrent 报告该种子是否在补整理时间窗内完成。
|
|
// qBittorrent 未提供 completion_on 时保守地返回 false。
|
|
func recentlyCompletedTorrent(torrent QBitTorrent, now time.Time) bool {
|
|
if torrent.CompletionOn <= 0 {
|
|
return false
|
|
}
|
|
completed := time.Unix(torrent.CompletionOn, 0)
|
|
return now.Sub(completed) <= completedTorrentCatchupWindow
|
|
}
|
|
|
|
func qbitTorrentCompleted(torrent QBitTorrent) bool {
|
|
if torrent.Progress < 1 {
|
|
return false
|
|
}
|
|
state := strings.ToLower(strings.TrimSpace(torrent.State))
|
|
switch state {
|
|
case "completed", "complete", "seeding", "uploading", "stalledup", "pausedup", "queuedup", "forcedup":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (d *DownloadService) completedTorrentCatchupRecorded(ctx context.Context, torrent QBitTorrent) bool {
|
|
if d == nil || d.repo == nil || d.repo.Setting == nil {
|
|
return false
|
|
}
|
|
key := completedTorrentCatchupSettingKey(torrent)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
value, err := d.repo.Setting.Get(ctx, key)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return parseBoolSetting(value, false)
|
|
}
|
|
|
|
func (d *DownloadService) markCompletedTorrentCatchupRecorded(ctx context.Context, torrent QBitTorrent) {
|
|
if d == nil || d.repo == nil || d.repo.Setting == nil {
|
|
return
|
|
}
|
|
key := completedTorrentCatchupSettingKey(torrent)
|
|
if key == "" {
|
|
return
|
|
}
|
|
if err := d.repo.Setting.Set(ctx, key, "true"); err != nil && d.log != nil {
|
|
d.log.Debug("mark completed torrent catchup failed",
|
|
zap.String("hash", torrent.Hash),
|
|
zap.String("name", torrent.Name),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func completedTorrentCatchupSettingKey(torrent QBitTorrent) string {
|
|
key := completedTorrentQueueKey(torrent)
|
|
if key == "" {
|
|
return ""
|
|
}
|
|
sum := sha1.Sum([]byte(key))
|
|
return completedTorrentCatchupSettingPrefix + fmt.Sprintf("%x", sum[:])
|
|
}
|
|
|
|
func (d *DownloadService) completedTorrentNotified(ctx context.Context, torrent QBitTorrent) bool {
|
|
if d == nil || d.repo == nil || d.repo.Setting == nil {
|
|
return false
|
|
}
|
|
key := completedTorrentNotifySettingKey(torrent)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
value, err := d.repo.Setting.Get(ctx, key)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return parseBoolSetting(value, false)
|
|
}
|
|
|
|
func (d *DownloadService) markCompletedTorrentNotified(ctx context.Context, torrent QBitTorrent) {
|
|
if d == nil || d.repo == nil || d.repo.Setting == nil {
|
|
return
|
|
}
|
|
key := completedTorrentNotifySettingKey(torrent)
|
|
if key == "" {
|
|
return
|
|
}
|
|
if err := d.repo.Setting.Set(ctx, key, "true"); err != nil && d.log != nil {
|
|
d.log.Debug("mark completed torrent notification failed",
|
|
zap.String("hash", torrent.Hash),
|
|
zap.String("name", torrent.Name),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func completedTorrentNotifySettingKey(torrent QBitTorrent) string {
|
|
key := completedTorrentQueueKey(torrent)
|
|
if key == "" {
|
|
return ""
|
|
}
|
|
sum := sha1.Sum([]byte(key))
|
|
return completedTorrentNotifySettingPrefix + fmt.Sprintf("%x", sum[:])
|
|
}
|
|
|
|
func completedTorrentQueueKey(torrent QBitTorrent) string {
|
|
hash := strings.ToLower(strings.TrimSpace(torrent.Hash))
|
|
if hash != "" {
|
|
owner := strings.ToLower(firstNonEmpty(torrent.ClientID, torrent.Source))
|
|
if owner != "" {
|
|
return owner + "|" + hash
|
|
}
|
|
return hash
|
|
}
|
|
parts := []string{torrent.Name, torrent.ContentPath, torrent.SavePath}
|
|
if owner := firstNonEmpty(torrent.ClientID, torrent.Source); owner != "" {
|
|
parts = append([]string{owner}, parts...)
|
|
}
|
|
for i := range parts {
|
|
parts[i] = strings.TrimSpace(parts[i])
|
|
}
|
|
key := strings.Join(parts, "|")
|
|
if strings.Trim(key, "|") == "" {
|
|
return ""
|
|
}
|
|
return strings.ToLower(key)
|
|
}
|
|
|
|
func (d *DownloadService) syncDownloadTaskProgress(ctx context.Context, torrent QBitTorrent, taskByKey map[string]model.DownloadTask) {
|
|
if d == nil || d.repo == nil || d.repo.DB == nil {
|
|
return
|
|
}
|
|
matched, ok := findMatchingTaskForTorrent(torrent, taskByKey)
|
|
if !ok {
|
|
return
|
|
}
|
|
status := torrent.State
|
|
if qbitTorrentCompleted(torrent) {
|
|
status = "completed"
|
|
}
|
|
if strings.TrimSpace(status) == "" {
|
|
status = matched.Status
|
|
}
|
|
updates := map[string]any{}
|
|
if math.Abs(float64(matched.Progress-torrent.Progress)) > 0.0001 {
|
|
updates["progress"] = torrent.Progress
|
|
}
|
|
if status != "" && status != matched.Status {
|
|
updates["status"] = status
|
|
}
|
|
if strings.TrimSpace(matched.DownloadClientID) == "" && strings.TrimSpace(torrent.ClientID) != "" {
|
|
updates["download_client_id"] = strings.TrimSpace(torrent.ClientID)
|
|
}
|
|
if strings.TrimSpace(matched.ExternalID) == "" && strings.TrimSpace(torrent.Hash) != "" {
|
|
updates["external_id"] = strings.TrimSpace(torrent.Hash)
|
|
}
|
|
if strings.TrimSpace(torrent.Source) != "" && matched.Source != strings.TrimSpace(torrent.Source) {
|
|
updates["source"] = strings.TrimSpace(torrent.Source)
|
|
}
|
|
if len(updates) == 0 {
|
|
return
|
|
}
|
|
_ = d.repo.DB.WithContext(ctx).Model(&model.DownloadTask{}).Where("id = ?", matched.ID).Updates(updates).Error
|
|
}
|
|
|
|
func tasksByIdentity(rows []model.DownloadTask) map[string]model.DownloadTask {
|
|
out := make(map[string]model.DownloadTask, len(rows))
|
|
for _, row := range rows {
|
|
key := downloadTaskIdentityKey(row.Title)
|
|
if key != "" {
|
|
out[key] = row
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tasksByTorrentIdentity(rows []model.DownloadTask) map[string]model.DownloadTask {
|
|
out := make(map[string]model.DownloadTask, len(rows)*4)
|
|
for _, row := range rows {
|
|
key := normalizeTorrentName(row.Title)
|
|
if key != "" {
|
|
setDownloadTaskIndex(out, key, row)
|
|
setDownloadTaskIndex(out, downloadTaskClientTitleKey(row.DownloadClientID, key), row)
|
|
}
|
|
if externalID := strings.TrimSpace(row.ExternalID); externalID != "" {
|
|
setDownloadTaskIndex(out, downloadTaskExternalKey(row.DownloadClientID, externalID), row)
|
|
setDownloadTaskIndex(out, downloadTaskAnyExternalKey(externalID), row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func setDownloadTaskIndex(index map[string]model.DownloadTask, key string, row model.DownloadTask) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
if _, exists := index[key]; !exists {
|
|
index[key] = row
|
|
}
|
|
}
|
|
|
|
func downloadTaskExternalKey(clientID, externalID string) string {
|
|
clientID = strings.ToLower(strings.TrimSpace(clientID))
|
|
externalID = strings.ToLower(strings.TrimSpace(externalID))
|
|
if clientID == "" || externalID == "" {
|
|
return ""
|
|
}
|
|
return "\x00external:" + clientID + ":" + externalID
|
|
}
|
|
|
|
func downloadTaskAnyExternalKey(externalID string) string {
|
|
externalID = strings.ToLower(strings.TrimSpace(externalID))
|
|
if externalID == "" {
|
|
return ""
|
|
}
|
|
return "\x00external-any:" + externalID
|
|
}
|
|
|
|
func downloadTaskClientTitleKey(clientID, titleKey string) string {
|
|
clientID = strings.ToLower(strings.TrimSpace(clientID))
|
|
titleKey = strings.TrimSpace(titleKey)
|
|
if clientID == "" || titleKey == "" {
|
|
return ""
|
|
}
|
|
return "\x00client-title:" + clientID + ":" + titleKey
|
|
}
|
|
|
|
func findMatchingTaskForTorrent(torrent QBitTorrent, taskByKey map[string]model.DownloadTask) (model.DownloadTask, bool) {
|
|
if row, ok := taskByKey[downloadTaskExternalKey(torrent.ClientID, torrent.Hash)]; ok {
|
|
return row, true
|
|
}
|
|
if row, ok := taskByKey[downloadTaskAnyExternalKey(torrent.Hash)]; ok {
|
|
if strings.TrimSpace(row.DownloadClientID) == "" || strings.TrimSpace(torrent.ClientID) == "" || row.DownloadClientID == torrent.ClientID {
|
|
return row, true
|
|
}
|
|
}
|
|
titleKey := normalizeTorrentName(torrent.Name)
|
|
if row, ok := taskByKey[downloadTaskClientTitleKey(torrent.ClientID, titleKey)]; ok {
|
|
return row, true
|
|
}
|
|
row, ok := findMatchingTaskByTorrentIdentity(torrent.Name, taskByKey)
|
|
if !ok {
|
|
return model.DownloadTask{}, false
|
|
}
|
|
if strings.TrimSpace(torrent.ClientID) != "" && strings.TrimSpace(row.DownloadClientID) != "" && row.DownloadClientID != torrent.ClientID {
|
|
return model.DownloadTask{}, false
|
|
}
|
|
return row, true
|
|
}
|
|
|
|
func findMatchingTaskByIdentity(title string, taskByKey map[string]model.DownloadTask) (model.DownloadTask, bool) {
|
|
key := downloadTaskIdentityKey(title)
|
|
if key == "" {
|
|
return model.DownloadTask{}, false
|
|
}
|
|
if row, ok := taskByKey[key]; ok {
|
|
return row, true
|
|
}
|
|
for currentKey, row := range taskByKey {
|
|
if strings.HasPrefix(currentKey, "\x00") {
|
|
continue
|
|
}
|
|
if strings.Contains(key, currentKey) || strings.Contains(currentKey, key) {
|
|
return row, true
|
|
}
|
|
}
|
|
return model.DownloadTask{}, false
|
|
}
|
|
|
|
func findMatchingTaskByTorrentIdentity(title string, taskByKey map[string]model.DownloadTask) (model.DownloadTask, bool) {
|
|
key := normalizeTorrentName(title)
|
|
if key == "" {
|
|
return model.DownloadTask{}, false
|
|
}
|
|
if row, ok := taskByKey[key]; ok {
|
|
return row, true
|
|
}
|
|
for currentKey, row := range taskByKey {
|
|
if strings.HasPrefix(currentKey, "\x00") {
|
|
continue
|
|
}
|
|
if strings.Contains(key, currentKey) || strings.Contains(currentKey, key) {
|
|
return row, true
|
|
}
|
|
}
|
|
return model.DownloadTask{}, false
|
|
}
|
|
|
|
func downloadTaskNeedsCompletion(task model.DownloadTask) bool {
|
|
if task.Progress < 1 {
|
|
return true
|
|
}
|
|
return strings.ToLower(strings.TrimSpace(task.Status)) != "completed"
|
|
}
|