153 lines
4.2 KiB
Go
153 lines
4.2 KiB
Go
package monitor
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pyed/transmission"
|
|
"transmission-telegram/internal/logger"
|
|
transmissionClient "transmission-telegram/internal/transmission"
|
|
)
|
|
|
|
// torrentState stores the state of a torrent for completion detection
|
|
type torrentState struct {
|
|
Status int
|
|
PercentDone float64
|
|
}
|
|
|
|
// Monitor monitors torrent completion via RPC API polling
|
|
type Monitor struct {
|
|
client transmissionClient.Client
|
|
logger *logger.Logger
|
|
states map[int]torrentState
|
|
statesMutex sync.RWMutex
|
|
chatID *int64
|
|
chatMutex sync.RWMutex
|
|
onComplete func(torrent *transmission.Torrent)
|
|
interval time.Duration
|
|
}
|
|
|
|
// NewMonitor creates a new torrent completion monitor
|
|
func NewMonitor(client transmissionClient.Client, log *logger.Logger, interval time.Duration) *Monitor {
|
|
return &Monitor{
|
|
client: client,
|
|
logger: log,
|
|
states: make(map[int]torrentState),
|
|
interval: interval,
|
|
}
|
|
}
|
|
|
|
// SetChatID sets the chat ID for notifications
|
|
func (m *Monitor) SetChatID(chatID int64) {
|
|
m.chatMutex.Lock()
|
|
m.chatID = &chatID
|
|
m.chatMutex.Unlock()
|
|
}
|
|
|
|
// SetOnComplete sets the callback function for completion notifications
|
|
func (m *Monitor) SetOnComplete(callback func(torrent *transmission.Torrent)) {
|
|
m.onComplete = callback
|
|
}
|
|
|
|
// Start starts the monitoring loop
|
|
func (m *Monitor) Start(ctx context.Context) {
|
|
ticker := time.NewTicker(m.interval)
|
|
defer ticker.Stop()
|
|
|
|
m.logger.Printf("[INFO] Starting torrent completion monitoring via RPC polling")
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
m.logger.Printf("[INFO] Stopping torrent completion monitoring")
|
|
return
|
|
case <-ticker.C:
|
|
m.checkCompletions()
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkCompletions checks for completed torrents
|
|
func (m *Monitor) checkCompletions() {
|
|
m.chatMutex.RLock()
|
|
if m.chatID == nil || *m.chatID == 0 {
|
|
m.chatMutex.RUnlock()
|
|
return
|
|
}
|
|
m.chatMutex.RUnlock()
|
|
|
|
torrents, err := m.client.GetTorrents()
|
|
if err != nil {
|
|
m.logger.Printf("[ERROR] Failed to get torrents for monitoring: %s", err)
|
|
return
|
|
}
|
|
|
|
m.statesMutex.Lock()
|
|
defer m.statesMutex.Unlock()
|
|
|
|
// Create a set of current torrent IDs for efficient lookup
|
|
// Use map[int]struct{} for better memory efficiency than map[int]bool
|
|
currentTorrentIDs := make(map[int]struct{}, len(torrents))
|
|
for _, torrent := range torrents {
|
|
currentTorrentIDs[torrent.ID] = struct{}{}
|
|
}
|
|
|
|
// Check each torrent for completion and update states
|
|
for _, torrent := range torrents {
|
|
prevState, exists := m.states[torrent.ID]
|
|
|
|
// Detect completion: transition from Downloading to Seeding with 100% done
|
|
// Optimized: only check if we were tracking this torrent
|
|
completed := false
|
|
if exists && prevState.Status == transmission.StatusDownloading {
|
|
// Case 1: Status changed from Downloading to Seeding (most common)
|
|
if torrent.Status == transmission.StatusSeeding && torrent.PercentDone >= 1.0 {
|
|
completed = true
|
|
} else if torrent.Status == transmission.StatusDownloading &&
|
|
prevState.PercentDone < 1.0 &&
|
|
torrent.PercentDone >= 1.0 {
|
|
// Case 2: Torrent reached 100% while still in Downloading status
|
|
completed = true
|
|
}
|
|
}
|
|
|
|
if completed && m.onComplete != nil {
|
|
// Call callback outside of lock to avoid potential deadlocks
|
|
// Use recover to protect against panics in callback
|
|
m.statesMutex.Unlock()
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
m.logger.Printf("[ERROR] Panic recovered in completion callback for torrent %d: %v", torrent.ID, r)
|
|
}
|
|
}()
|
|
m.onComplete(torrent)
|
|
m.logger.Printf("[INFO] Torrent completed: %s (ID: %d)", torrent.Name, torrent.ID)
|
|
}()
|
|
m.statesMutex.Lock()
|
|
}
|
|
|
|
// Update state - only track downloading torrents to save memory
|
|
if torrent.Status == transmission.StatusDownloading ||
|
|
torrent.Status == transmission.StatusDownloadPending {
|
|
m.states[torrent.ID] = torrentState{
|
|
Status: torrent.Status,
|
|
PercentDone: torrent.PercentDone,
|
|
}
|
|
} else {
|
|
// Remove completed or stopped torrents from tracking
|
|
delete(m.states, torrent.ID)
|
|
}
|
|
}
|
|
|
|
// Clean up states for torrents that no longer exist
|
|
// More efficient: iterate over states map instead of currentTorrentIDs
|
|
for torrentID := range m.states {
|
|
if _, exists := currentTorrentIDs[torrentID]; !exists {
|
|
delete(m.states, torrentID)
|
|
}
|
|
}
|
|
}
|
|
|