mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 14:26:37 +08:00
fix cloud library scale and playback compatibility
Root cause: display-only cloud library filtering was reused by scan jobs, merged cloud mounts could be skipped, and OpenList listing relied on WebDAV/first-page behavior that could cap huge directories around 100 items. Cloud scans also let new items consume probe budget before existing rows with missing track/artwork metadata.\n\nChanges:\n- split display filtering from scannable cloud filtering so merged cloud mounts still scan\n- add OpenList API pagination with WebDAV fallback\n- prioritize existing cloud media missing metadata before new imports\n- restrict automatic cloud sync to one successful 19:00-21:00 daily window and keep manual scan immediate\n- disable startup cloud full-scan by default, with an explicit opt-in setting\n- improve Emby/cloud playback compatibility and cache/search/test coverage from the continued work
This commit is contained in:
@@ -47,6 +47,7 @@ type SchedulerService struct {
|
||||
storageCfg *StorageConfigService
|
||||
hub *Hub
|
||||
cacheDir string
|
||||
now func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
stopCh chan struct{}
|
||||
@@ -64,6 +65,15 @@ type scheduledJob struct {
|
||||
|
||||
type schedulerManualRunKey struct{}
|
||||
|
||||
const (
|
||||
cloudAutoSyncEnabledKey = "cloud.auto_sync_enabled"
|
||||
cloudSyncIntervalSecondsKey = "cloud.sync_interval_seconds"
|
||||
cloudLastAutoSyncDateKey = "cloud.last_auto_sync_date"
|
||||
cloudAutoSyncWindowStartHour = 19
|
||||
cloudAutoSyncWindowEndHour = 21
|
||||
cloudAutoSyncCompletedDateForm = "2006-01-02"
|
||||
)
|
||||
|
||||
// NewSchedulerService is the constructor.
|
||||
func NewSchedulerService(
|
||||
log *zap.Logger,
|
||||
@@ -84,6 +94,7 @@ func NewSchedulerService(
|
||||
storageCfg: storageCfg,
|
||||
hub: hub,
|
||||
cacheDir: cacheDir,
|
||||
now: time.Now,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@@ -214,7 +225,7 @@ func (s *SchedulerService) loopWithInitialDelay(ctx context.Context, j *schedule
|
||||
func (s *SchedulerService) runOnce(ctx context.Context, j *scheduledJob) error {
|
||||
err := j.run(ctx)
|
||||
s.mu.Lock()
|
||||
j.lastRun = time.Now()
|
||||
j.lastRun = s.currentTime()
|
||||
if err != nil {
|
||||
j.lastErr = err.Error()
|
||||
} else {
|
||||
@@ -336,24 +347,35 @@ func (s *SchedulerService) cloudUploadInterval(ctx context.Context) time.Duratio
|
||||
// /api/cloud/play 302/proxy.
|
||||
func (s *SchedulerService) jobSyncCloudLibraries(ctx context.Context) error {
|
||||
manual, _ := ctx.Value(schedulerManualRunKey{}).(bool)
|
||||
if s.scanner == nil || (!manual && !s.autoCloudSyncEnabled(ctx)) {
|
||||
if s.scanner == nil || (!manual && !s.autoCloudSyncDue(ctx, s.currentTime())) {
|
||||
return nil
|
||||
}
|
||||
libs, err := s.repo.Library.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
libs = FilterScannableCloudLibraries(ctx, s.repo, libs)
|
||||
var firstErr error
|
||||
for _, l := range libs {
|
||||
if !l.Enabled {
|
||||
continue
|
||||
}
|
||||
if _, _, ok := parseCloudLibraryPath(l.Path); !ok {
|
||||
if _, ok := ParseCloudLibraryMount(l.Path); !ok {
|
||||
continue
|
||||
}
|
||||
if _, err := s.scanner.ScanLibraryWithoutAutoScrape(ctx, l.ID); err != nil {
|
||||
s.log.Warn("cloud sync failed", zap.String("library", l.ID), zap.Error(err))
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
return firstErr
|
||||
}
|
||||
if !manual {
|
||||
_ = s.markCloudAutoSyncCompleted(ctx, s.currentTime())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -361,19 +383,45 @@ func (s *SchedulerService) autoCloudSyncEnabled(ctx context.Context) bool {
|
||||
if s.repo == nil || s.repo.Setting == nil {
|
||||
return false
|
||||
}
|
||||
v, err := s.repo.Setting.Get(ctx, "cloud.auto_sync_enabled")
|
||||
v, err := s.repo.Setting.Get(ctx, cloudAutoSyncEnabledKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return parseBoolSetting(v, false)
|
||||
}
|
||||
|
||||
func (s *SchedulerService) autoCloudSyncDue(ctx context.Context, now time.Time) bool {
|
||||
if !s.autoCloudSyncEnabled(ctx) || !cloudAutoSyncInWindow(now) {
|
||||
return false
|
||||
}
|
||||
if s.repo == nil || s.repo.Setting == nil {
|
||||
return true
|
||||
}
|
||||
last, err := s.repo.Setting.Get(ctx, cloudLastAutoSyncDateKey)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return strings.TrimSpace(last) != now.Format(cloudAutoSyncCompletedDateForm)
|
||||
}
|
||||
|
||||
func cloudAutoSyncInWindow(now time.Time) bool {
|
||||
hour := now.In(time.Local).Hour()
|
||||
return hour >= cloudAutoSyncWindowStartHour && hour < cloudAutoSyncWindowEndHour
|
||||
}
|
||||
|
||||
func (s *SchedulerService) markCloudAutoSyncCompleted(ctx context.Context, now time.Time) error {
|
||||
if s.repo == nil || s.repo.Setting == nil {
|
||||
return nil
|
||||
}
|
||||
return s.repo.Setting.Set(ctx, cloudLastAutoSyncDateKey, now.Format(cloudAutoSyncCompletedDateForm))
|
||||
}
|
||||
|
||||
func (s *SchedulerService) cloudSyncInterval(ctx context.Context) time.Duration {
|
||||
const fallback = 30 * time.Minute
|
||||
if s.repo == nil || s.repo.Setting == nil {
|
||||
return fallback
|
||||
}
|
||||
v, err := s.repo.Setting.Get(ctx, "cloud.sync_interval_seconds")
|
||||
v, err := s.repo.Setting.Get(ctx, cloudSyncIntervalSecondsKey)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
@@ -387,6 +435,13 @@ func (s *SchedulerService) cloudSyncInterval(ctx context.Context) time.Duration
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func (s *SchedulerService) currentTime() time.Time {
|
||||
if s != nil && s.now != nil {
|
||||
return s.now()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// periodicScanEnabled reports whether the operator opted into periodic full
|
||||
// library re-scans. Defaults to false so the incremental watcher is the only
|
||||
// thing touching the disk under normal operation.
|
||||
|
||||
Reference in New Issue
Block a user