diff --git a/cmd/jukebox/main.go b/cmd/jukebox/main.go index 2ac5ddb..59e77f4 100644 --- a/cmd/jukebox/main.go +++ b/cmd/jukebox/main.go @@ -230,6 +230,7 @@ func triggerAutoCopy(cp *copier.Copier, cfg *config.Config, info disk.DiskInfo) opts.ReserveFreeGB = p.ReserveFreeGB opts.FileSelectMode = config.FileSelectMode(p.FileSelectMode) opts.Transcode = p.Transcode + opts.ShuffleDepth = p.ShuffleDepth if p.OverwriteMode != "" { opts.OverwriteMode = config.OverwriteMode(p.OverwriteMode) } diff --git a/internal/api/handlers_copy.go b/internal/api/handlers_copy.go index c52db4b..e2d669e 100644 --- a/internal/api/handlers_copy.go +++ b/internal/api/handlers_copy.go @@ -26,6 +26,7 @@ func (s *Server) copyOptions(cfg *config.Config, diskInfo disk.DiskInfo, overwri opts.ReserveFreeGB = p.ReserveFreeGB opts.FileSelectMode = config.FileSelectMode(p.FileSelectMode) opts.Transcode = p.Transcode + opts.ShuffleDepth = p.ShuffleDepth } else { opts.DestFolder = cfg.DestFolder opts.ReserveFreeGB = cfg.ReserveFreeGB diff --git a/internal/copier/copier.go b/internal/copier/copier.go index 85b95e2..d235d11 100644 --- a/internal/copier/copier.go +++ b/internal/copier/copier.go @@ -33,6 +33,8 @@ type Options struct { OverwriteMode config.OverwriteMode FileSelectMode config.FileSelectMode Transcode *disk.TranscodeProfile // nil = не транскодировать + // ShuffleDepth: -1=выкл, 0=файлы вразнобой, 1+=группировка по папке на глубине N + ShuffleDepth int } type Copier struct { @@ -254,8 +256,7 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database return } - // случайный порядок — выбираем что копировать до начала копирования - rand.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) + files = applyShuffleDepth(files, opts.ShuffleDepth) _, free, err := disk.DiskUsage(opts.MountPath) if err != nil { @@ -723,3 +724,52 @@ func copyFile(ctx context.Context, src, dst string) error { } return nil } + +// applyShuffleDepth упорядочивает файлы по заданной глубине шафлера. +// depth < 0 → оригинальный порядок (без шафла) +// depth == 0 → все файлы перемешиваются случайно +// depth >= 1 → файлы группируются по папке на уровне depth от корня /media, +// группы перемешиваются, внутри каждой группы порядок сохраняется. +func applyShuffleDepth(files []fileEntry, depth int) []fileEntry { + if depth < 0 || len(files) == 0 { + return files + } + if depth == 0 { + rand.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) + return files + } + + type group struct { + key string + files []fileEntry + } + groupMap := make(map[string]*group, 64) + var order []string + for _, f := range files { + key := folderKeyAtDepth(f.relPath, depth) + if _, ok := groupMap[key]; !ok { + groupMap[key] = &group{key: key} + order = append(order, key) + } + groupMap[key].files = append(groupMap[key].files, f) + } + rand.Shuffle(len(order), func(i, j int) { order[i], order[j] = order[j], order[i] }) + + result := make([]fileEntry, 0, len(files)) + for _, key := range order { + result = append(result, groupMap[key].files...) + } + return result +} + +// folderKeyAtDepth возвращает путь к папке глубины depth из relPath. +// relPath вида "anime/Naruto/Season1/ep01.mkv", depth=2 → "anime/Naruto" +func folderKeyAtDepth(relPath string, depth int) string { + relPath = filepath.ToSlash(relPath) + parts := strings.Split(relPath, "/") + maxDepth := len(parts) - 1 // последний элемент — имя файла + if depth >= maxDepth { + depth = maxDepth + } + return strings.Join(parts[:depth], "/") +} diff --git a/internal/disk/profile.go b/internal/disk/profile.go index 4b0f8bc..bef3d14 100644 --- a/internal/disk/profile.go +++ b/internal/disk/profile.go @@ -12,6 +12,8 @@ type DiskProfile struct { FileSelectMode string `json:"file_select_mode"` ReserveFreeGB float64 `json:"reserve_free_gb"` AutoCopy bool `json:"auto_copy"` + // ShuffleDepth: -1=выкл, 0=файлы вразнобой, 1+=папки на глубине N от корня /media + ShuffleDepth int `json:"shuffle_depth"` // nil = не транскодировать видео Transcode *TranscodeProfile `json:"transcode,omitempty"` @@ -66,5 +68,6 @@ func DefaultProfile() *DiskProfile { FileSelectMode: "new", ReserveFreeGB: 2.0, AutoCopy: false, + ShuffleDepth: -1, } } diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html index f878aeb..6b6856c 100644 --- a/web/templates/dashboard.html +++ b/web/templates/dashboard.html @@ -209,6 +209,11 @@ function renderProfile(disk) { ${sel('file_select_mode', p.file_select_mode || 'new', [['new','Только новые'],['all','Все подходящие']])} +