mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
185 lines
6.5 KiB
Go
185 lines
6.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
)
|
|
|
|
type cloudScanImportRequest struct {
|
|
provider string
|
|
candidates []cloudCandidate
|
|
existingMedia map[string]existingCloudMedia
|
|
writeBatch *localMediaWriteBatch
|
|
probeBudget *int
|
|
defaultRootID string
|
|
progress *cloudScanProgressState
|
|
result *ScanResult
|
|
}
|
|
|
|
type cloudScanImportResult struct {
|
|
seen map[string]struct{}
|
|
touchedLibraryIDs []string
|
|
scopeLibraryIDs []string
|
|
}
|
|
|
|
type cloudLibraryScanCompletion struct {
|
|
libraryID string
|
|
touchedLibraryIDs []string
|
|
result *ScanResult
|
|
progress *cloudScanProgressState
|
|
autoScrape bool
|
|
}
|
|
|
|
func (s *ScannerService) scanCloudLibrary(ctx context.Context, lib *model.Library, mount CloudMountInfo, autoScrape bool) (*ScanResult, error) {
|
|
return s.scanCloudLibraryWithRoot(ctx, lib, mount, "", autoScrape)
|
|
}
|
|
|
|
func (s *ScannerService) scanCloudLibraryRoot(ctx context.Context, lib *model.Library, root *model.LibraryRoot, mount CloudMountInfo, autoScrape bool) (*ScanResult, error) {
|
|
return s.scanCloudLibraryWithRoot(ctx, lib, mount, libraryRootID(root), autoScrape)
|
|
}
|
|
|
|
func (s *ScannerService) scanCloudLibraryWithRoot(ctx context.Context, lib *model.Library, mount CloudMountInfo, defaultRootID string, autoScrape bool) (*ScanResult, error) {
|
|
res := &ScanResult{LibraryID: lib.ID}
|
|
if s.storage == nil {
|
|
return res, fmt.Errorf("cloud storage service unavailable")
|
|
}
|
|
|
|
cfg, err := s.repo.StorageConfig.Get(ctx, mount.Provider)
|
|
if err != nil || cfg == nil {
|
|
return res, fmt.Errorf("storage config not found: %s", mount.Provider)
|
|
}
|
|
if !cfg.Enabled {
|
|
return res, fmt.Errorf("storage %s is disabled", mount.Provider)
|
|
}
|
|
typ := mount.Provider
|
|
rootDir := mount.ScanDir
|
|
rootDisplayDir := mount.DisplayDir
|
|
autoCategoryRoot := cloudRootMountNeedsAutoCategory(mount)
|
|
scopeIDs := s.cloudScanLibraryScopeIDs(ctx, lib, mount)
|
|
progress := newCloudScanProgressState()
|
|
progress.publish(s, lib.ID, res, "listing", true)
|
|
candidates, err := s.collectCloudScanCandidates(ctx, lib, cloudScanCandidateRequest{
|
|
provider: typ,
|
|
rootDir: rootDir,
|
|
rootDisplayDir: rootDisplayDir,
|
|
autoCategoryRoot: autoCategoryRoot,
|
|
progress: progress,
|
|
result: res,
|
|
})
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
existingMedia, err := s.existingCloudMediaSnapshotForLibraries(ctx, scopeIDs)
|
|
if err != nil {
|
|
s.log.Warn("load existing cloud media snapshot failed", zap.String("library_id", lib.ID), zap.Error(err))
|
|
existingMedia = nil
|
|
}
|
|
sortCloudCandidatesByRefreshPriority(candidates, existingMedia)
|
|
writeBatch := newLocalMediaWriteBatch(s, ctx, res, 100)
|
|
probeBudget := maxCloudMediaProbeQueuePerScan
|
|
imported, err := s.importCloudScanCandidates(ctx, lib, cloudScanImportRequest{
|
|
provider: typ,
|
|
candidates: candidates,
|
|
existingMedia: existingMedia,
|
|
writeBatch: writeBatch,
|
|
probeBudget: &probeBudget,
|
|
defaultRootID: defaultRootID,
|
|
progress: progress,
|
|
result: res,
|
|
})
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
scopeIDs = appendUniqueLibraryIDs(scopeIDs, imported.scopeLibraryIDs...)
|
|
writeBatch.Flush()
|
|
var removed int64
|
|
if defaultRootID != "" {
|
|
removed, err = s.pruneMissingCloudMediaForRoot(ctx, lib.ID, defaultRootID, imported.seen)
|
|
} else {
|
|
removed, err = s.pruneMissingCloudMediaForLibraries(ctx, scopeIDs, imported.seen)
|
|
}
|
|
if err != nil {
|
|
s.log.Warn("prune missing cloud media failed", zap.String("library_id", lib.ID), zap.Error(err))
|
|
} else {
|
|
res.Removed = removed
|
|
}
|
|
s.completeCloudLibraryScan(ctx, cloudLibraryScanCompletion{
|
|
libraryID: lib.ID,
|
|
touchedLibraryIDs: imported.touchedLibraryIDs,
|
|
result: res,
|
|
progress: progress,
|
|
autoScrape: autoScrape,
|
|
})
|
|
return res, nil
|
|
}
|
|
|
|
type cloudScanTarget struct {
|
|
lib *model.Library
|
|
rootID string
|
|
}
|
|
|
|
func (s *ScannerService) importCloudScanCandidates(ctx context.Context, rootLib *model.Library, req cloudScanImportRequest) (cloudScanImportResult, error) {
|
|
imported := cloudScanImportResult{
|
|
seen: make(map[string]struct{}),
|
|
touchedLibraryIDs: []string{},
|
|
scopeLibraryIDs: []string{},
|
|
}
|
|
targetLibs := map[string]cloudScanTarget{"": {lib: rootLib, rootID: req.defaultRootID}}
|
|
for _, candidate := range req.candidates {
|
|
select {
|
|
case <-ctx.Done():
|
|
return imported, ctx.Err()
|
|
default:
|
|
}
|
|
target := targetLibs[""]
|
|
if candidate.categoryDisplayDir != "" {
|
|
categoryKey := candidate.categoryDisplayDir + "\x00" + candidate.categoryScanDir
|
|
if cached, ok := targetLibs[categoryKey]; ok {
|
|
target = cached
|
|
} else if categoryTarget, err := s.ensureCloudAutoCategoryTarget(ctx, rootLib, req.provider, candidate.categoryDisplayDir, candidate.categoryScanDir); err == nil && categoryTarget.Library != nil {
|
|
target = cloudScanTarget{lib: categoryTarget.Library, rootID: categoryTarget.RootID}
|
|
targetLibs[categoryKey] = target
|
|
imported.scopeLibraryIDs = appendUniqueLibraryIDs(imported.scopeLibraryIDs, categoryTarget.Library.ID)
|
|
} else if err != nil {
|
|
s.log.Warn("ensure cloud auto category library failed",
|
|
zap.String("library_id", rootLib.ID),
|
|
zap.String("provider", req.provider),
|
|
zap.String("category", candidate.categoryDisplayDir),
|
|
zap.String("scan_dir", candidate.categoryScanDir),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
targetLib := target.lib
|
|
if targetLib == nil {
|
|
targetLib = rootLib
|
|
}
|
|
imported.touchedLibraryIDs = appendUniqueLibraryIDs(imported.touchedLibraryIDs, targetLib.ID)
|
|
imported.seen[candidate.path] = struct{}{}
|
|
s.ingestCloudFile(ctx, targetLib, target.rootID, req.provider, candidate.ref, candidate.path, candidate.name, candidate.size, candidate.localMeta, req.existingMedia, req.writeBatch, req.probeBudget, req.result)
|
|
req.progress.publish(s, rootLib.ID, req.result, "importing", req.result.Visited == 1 || req.result.Visited%100 == 0)
|
|
}
|
|
return imported, nil
|
|
}
|
|
|
|
func (s *ScannerService) completeCloudLibraryScan(ctx context.Context, req cloudLibraryScanCompletion) {
|
|
publishCloudScanFinished(s, req.libraryID, req.result, req.progress)
|
|
s.invalidateMediaCache(ctx)
|
|
targetIDs := appendUniqueLibraryIDs(req.touchedLibraryIDs, req.libraryID)
|
|
for _, targetID := range targetIDs {
|
|
s.maybeGenerateSTRMAfterScan(targetID)
|
|
}
|
|
if scanHasImportChanges(req.result) && req.autoScrape && s.scraper != nil && s.scraper.AnyEnabled() && s.autoScrapeEnabled(ctx) {
|
|
for _, targetID := range targetIDs {
|
|
s.startAutoScrape(ctx, targetID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func scanHasImportChanges(res *ScanResult) bool {
|
|
return res != nil && (res.Added > 0 || res.Updated > 0 || res.Removed > 0)
|
|
}
|