mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
190 lines
5.3 KiB
Go
190 lines
5.3 KiB
Go
// Package service — periodic scheduled jobs.
|
|
//
|
|
// SchedulerService runs recurring background jobs that keep the
|
|
// library up-to-date without operator intervention:
|
|
//
|
|
// library_scan every 24 h — optional full re-scan for local libraries;
|
|
// filesystem watchers handle normal changes.
|
|
// organize_source opt-in — organize the configured staging folder.
|
|
// transcode_cleanup every 24 h — purge HLS transcode artefacts
|
|
// older than 24 h.
|
|
// segment_prewarm every 6 h — fill IntroDB skip segments for queryable media.
|
|
//
|
|
// Each job runs at most once at a time (an in-flight run blocks the
|
|
// next tick). All work happens on a long-lived background context so
|
|
// the operator can keep clicking around the UI while the watchdog runs.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/helper"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
)
|
|
|
|
// SchedulerService runs the periodic jobs.
|
|
type SchedulerService struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
scanner *ScannerService
|
|
transcoder *TranscoderService
|
|
organizer *OrganizerService
|
|
organizePipeline *OrganizePipelineService
|
|
hub *Hub
|
|
tasks *TaskTrackerService
|
|
expiryWatcher *TelegramExpiryWatcher
|
|
cacheDir string
|
|
now func() time.Time
|
|
|
|
imagesPolicyProvider func() ImageCachePolicy
|
|
|
|
segments *MediaSegmentService
|
|
|
|
mu sync.Mutex
|
|
stopCh chan struct{}
|
|
jobs []*scheduledJob
|
|
}
|
|
|
|
var (
|
|
ErrSchedulerJobNotFound = errors.New("scheduled job not found")
|
|
ErrSchedulerJobAlreadyRunning = errors.New("scheduled job already running")
|
|
)
|
|
|
|
func (s *SchedulerService) SetTaskTracker(tasks *TaskTrackerService) {
|
|
s.tasks = tasks
|
|
}
|
|
|
|
func (s *SchedulerService) SetOrganizePipeline(pipeline *OrganizePipelineService) {
|
|
s.organizePipeline = pipeline
|
|
}
|
|
|
|
// SetExpiryWatcher 注入账号到期巡检。未注入时(例如测试)该任务不注册。
|
|
func (s *SchedulerService) SetExpiryWatcher(watcher *TelegramExpiryWatcher) {
|
|
s.expiryWatcher = watcher
|
|
}
|
|
|
|
// ImageCachePolicy 是一次图片缓存清理要用的策略(全部为 0 表示不做任何清理)。
|
|
type ImageCachePolicy struct {
|
|
TotalBytes int64
|
|
OriginalsBytes int64
|
|
OriginalsAge time.Duration
|
|
}
|
|
|
|
func (s *SchedulerService) SetImageCachePolicyProvider(fn func() ImageCachePolicy) {
|
|
s.imagesPolicyProvider = fn
|
|
}
|
|
|
|
func (s *SchedulerService) imageCachePolicy() ImageCachePolicy {
|
|
if s.imagesPolicyProvider != nil {
|
|
return s.imagesPolicyProvider()
|
|
}
|
|
return ImageCachePolicy{}
|
|
}
|
|
|
|
// scheduledJob is one recurring task.
|
|
type scheduledJob struct {
|
|
name string
|
|
interval time.Duration
|
|
run func(ctx context.Context) error
|
|
lastRun time.Time
|
|
lastErr string
|
|
running bool
|
|
started time.Time
|
|
}
|
|
|
|
type schedulerManualRunKey struct{}
|
|
|
|
const localLastPeriodicScanDateKey = "scan.last_periodic_date"
|
|
|
|
// NewSchedulerService is the constructor.
|
|
func NewSchedulerService(
|
|
log *zap.Logger,
|
|
repo *repository.Container,
|
|
scanner *ScannerService,
|
|
transcoder *TranscoderService,
|
|
organizer *OrganizerService,
|
|
hub *Hub,
|
|
cacheDir string,
|
|
) *SchedulerService {
|
|
return &SchedulerService{
|
|
log: log,
|
|
repo: repo,
|
|
scanner: scanner,
|
|
transcoder: transcoder,
|
|
organizer: organizer,
|
|
hub: hub,
|
|
cacheDir: cacheDir,
|
|
now: time.Now,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start kicks off every job in its own goroutine and returns immediately.
|
|
func (s *SchedulerService) Start(ctx context.Context) {
|
|
s.jobs = []*scheduledJob{
|
|
{
|
|
name: "library_scan",
|
|
interval: 24 * time.Hour,
|
|
run: s.jobScanLibraries,
|
|
},
|
|
{
|
|
name: "organize_source",
|
|
interval: s.organizeSourceInterval(ctx),
|
|
run: s.jobOrganizeSource,
|
|
},
|
|
{
|
|
name: "transcode_cleanup",
|
|
interval: 24 * time.Hour,
|
|
run: s.jobCleanTranscodeCache,
|
|
},
|
|
{
|
|
name: "image_cache_cleanup",
|
|
interval: 1 * time.Hour,
|
|
run: s.jobCleanImageCache,
|
|
},
|
|
}
|
|
// 片头预热只在注入了 Segments 时注册,避免测试跑无转外网任务。
|
|
if s.segments != nil {
|
|
s.jobs = append(s.jobs, &scheduledJob{
|
|
name: "segment_prewarm",
|
|
interval: segmentPrewarmJobInterval,
|
|
run: s.jobSegmentPrewarm,
|
|
})
|
|
}
|
|
// 到期提醒只在配置了巡检器时注册,避免测试与未启用通知的部署跑空转任务。
|
|
if s.expiryWatcher != nil {
|
|
s.jobs = append(s.jobs, &scheduledJob{
|
|
name: "telegram_expiry_warning",
|
|
interval: 24 * time.Hour,
|
|
run: s.jobTelegramExpiryWarning,
|
|
})
|
|
}
|
|
for _, j := range s.jobs {
|
|
initialDelay := 15 * time.Second
|
|
if j.name == "library_scan" || j.name == "organize_source" || j.name == "segment_prewarm" {
|
|
// 重启后不立即整库重扫/整理/预热:更新窗口恰是登录高峰,
|
|
// 15 秒即全量 walk + ffprobe 曾把 CPU/磁盘打满导致无法登录。
|
|
// 首轮等满一个完整周期再跑,平时节奏不变。
|
|
initialDelay = j.interval
|
|
}
|
|
helper.Go(s.log, "scheduler.loop."+j.name, func() { s.loopWithInitialDelay(ctx, j, initialDelay) })
|
|
}
|
|
}
|
|
|
|
// Stop signals every job loop to exit on the next tick.
|
|
func (s *SchedulerService) Stop() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
select {
|
|
case <-s.stopCh:
|
|
// already closed
|
|
default:
|
|
close(s.stopCh)
|
|
}
|
|
}
|