diff --git a/gridplayer/dialogs/settings.py b/gridplayer/dialogs/settings.py index 67a9c878..bf736501 100644 --- a/gridplayer/dialogs/settings.py +++ b/gridplayer/dialogs/settings.py @@ -307,6 +307,8 @@ def fill_repeatMode(self): VideoRepeat.SINGLE_FILE: self.tr("Single File"), VideoRepeat.DIR: self.tr("Directory"), VideoRepeat.DIR_SHUFFLE: self.tr("Directory (Shuffle)"), + VideoRepeat.REC_DIR: self.tr("Recursive Directory"), + VideoRepeat.REC_DIR_SHUFFLE: self.tr("Recursive Directory (Shuffle)"), } _fill_combo_box(self.repeatMode, repeat_modes) diff --git a/gridplayer/params/actions.py b/gridplayer/params/actions.py index 6ca6a62c..07c9229a 100644 --- a/gridplayer/params/actions.py +++ b/gridplayer/params/actions.py @@ -288,6 +288,13 @@ "check_if": ("is_active_param_set_to", "repeat_mode", VideoRepeat.DIR), "show_if": "is_active_local_file", }, + "Repeat Directory Recursively": { + "title": translate("Actions", "Repeat Directory Recursively"), + "icon": "loop-dir", + "func": ("active", "set_repeat_mode", VideoRepeat.REC_DIR), + "check_if": ("is_active_param_set_to", "repeat_mode", VideoRepeat.REC_DIR), + "show_if": "is_active_local_file", + }, "Repeat Directory (Shuffle)": { "title": translate("Actions", "Repeat Directory (Shuffle)"), "icon": "loop-dir-shuffle", @@ -299,6 +306,17 @@ ), "show_if": "is_active_local_file", }, + "Repeat Directory Recursively (Shuffle)": { + "title": translate("Actions", "Repeat Directory Recursively (Shuffle)"), + "icon": "loop-dir-shuffle", + "func": ("active", "set_repeat_mode", VideoRepeat.REC_DIR_SHUFFLE), + "check_if": ( + "is_active_param_set_to", + "repeat_mode", + VideoRepeat.REC_DIR_SHUFFLE, + ), + "show_if": "is_active_local_file", + }, "Faster": { "title": translate("Actions", "Faster"), "key": "C", @@ -934,12 +952,25 @@ "func": ("all", "set_repeat_mode", VideoRepeat.DIR), "show_if": "is_any_videos_local_file", }, + "Repeat Directory Recursively [ALL]": { + "title": translate("Actions", "Repeat Directory Recursively"), + "icon": "loop-dir", + "func": ("all", "set_repeat_mode", VideoRepeat.REC_DIR), + "show_if": "is_any_videos_local_file", + }, + "Repeat Directory (Shuffle) [ALL]": { "title": translate("Actions", "Repeat Directory (Shuffle)"), "icon": "loop-dir-shuffle", "func": ("all", "set_repeat_mode", VideoRepeat.DIR_SHUFFLE), "show_if": "is_any_videos_local_file", }, + "Repeat Directory Recursively (Shuffle) [ALL]": { + "title": translate("Actions", "Repeat Directory Recursively (Shuffle)"), + "icon": "loop-dir-shuffle", + "func": ("all", "set_repeat_mode", VideoRepeat.REC_DIR_SHUFFLE), + "show_if": "is_any_videos_local_file", + }, "Faster [ALL]": { "title": translate("Actions", "Faster"), "key": "Shift+C", diff --git a/gridplayer/params/menu.py b/gridplayer/params/menu.py index 0e75e332..00a47569 100644 --- a/gridplayer/params/menu.py +++ b/gridplayer/params/menu.py @@ -164,7 +164,9 @@ "---", "Repeat Single File", "Repeat Directory", + "Repeat Directory Recursively", "Repeat Directory (Shuffle)", + "Repeat Directory Recursively (Shuffle)", ), ( "Speed", @@ -294,7 +296,9 @@ "---", "Repeat Single File [ALL]", "Repeat Directory [ALL]", + "Repeat Directory Recursively [ALL]", "Repeat Directory (Shuffle) [ALL]", + "Repeat Directory Recursively (Shuffle) [ALL]", ), ( "Speed", diff --git a/gridplayer/params/static.py b/gridplayer/params/static.py index 3f28e912..7f34b484 100644 --- a/gridplayer/params/static.py +++ b/gridplayer/params/static.py @@ -54,6 +54,8 @@ class VideoRepeat(AutoName): SINGLE_FILE = auto() DIR = auto() DIR_SHUFFLE = auto() + REC_DIR = auto() + REC_DIR_SHUFFLE = auto() class VideoDriver(AutoName): diff --git a/gridplayer/utils/next_file.py b/gridplayer/utils/next_file.py index 225abd97..7cf24e7a 100644 --- a/gridplayer/utils/next_file.py +++ b/gridplayer/utils/next_file.py @@ -1,44 +1,43 @@ import random from pathlib import Path -from typing import Optional +from typing import Optional, List from gridplayer.params.extensions import SUPPORTED_MEDIA_EXT -def next_video_file(file: Path, is_shuffle=False) -> Optional[Path]: - siblings = _file_siblings(file) - - if is_shuffle: - random.shuffle(siblings) - - try: - next_id = siblings.index(file) + 1 - except ValueError: - return None - - if next_id >= len(siblings): - next_id = 0 - - return siblings[next_id] - - -def previous_video_file(file: Path) -> Optional[Path]: - siblings = _file_siblings(file) - - try: - next_id = siblings.index(file) - 1 - except ValueError: - return None - - if next_id >= len(siblings): - next_id = 0 - - return siblings[next_id] - - -def _file_siblings(file: Path): - return sorted( - f - for f in file.parent.iterdir() - if f.is_file() and f.suffix[1:].lower() in SUPPORTED_MEDIA_EXT - ) +class VideoNavigator: + def __init__(self, start_file: Path, is_shuffle: bool, is_recursive: bool): + self.original_dir = start_file.parent + if is_recursive: + self.siblings = get_file_siblings(self.original_dir, is_recursive = True) + else: + self.siblings = get_file_siblings(self.original_dir, is_recursive = False) + if is_shuffle: + random.shuffle(self.siblings) + self.current_index = self.siblings.index(start_file) if start_file in self.siblings else -1 + + def next_video_file(self) -> Optional[Path]: + if not self.siblings or self.current_index == -1: + return None + self.current_index = (self.current_index + 1) % len(self.siblings) + return self.siblings[self.current_index] + + def previous_video_file(self) -> Optional[Path]: + if not self.siblings or self.current_index == -1: + return None + self.current_index = (self.current_index - 1) % len(self.siblings) + return self.siblings[self.current_index] + +def get_file_siblings(directory: Path, is_recursive: bool = False) -> List[Path]: + if is_recursive: + return sorted( + f + for f in directory.rglob("*") # Recursively includes subdirectories + if f.is_file() and f.suffix[1:].lower() in SUPPORTED_MEDIA_EXT + ) + else: + return sorted( + f + for f in directory.iterdir() # Only includes top-level files + if f.is_file() and f.suffix[1:].lower() in SUPPORTED_MEDIA_EXT + ) \ No newline at end of file diff --git a/gridplayer/widgets/video_block.py b/gridplayer/widgets/video_block.py index b102bd3b..c731d2b3 100644 --- a/gridplayer/widgets/video_block.py +++ b/gridplayer/widgets/video_block.py @@ -34,7 +34,7 @@ ) from gridplayer.settings import Settings from gridplayer.utils.libvlc_options_parser import get_vlc_options -from gridplayer.utils.next_file import next_video_file, previous_video_file +from gridplayer.utils.next_file import VideoNavigator, get_file_siblings from gridplayer.utils.qt import qt_connect, translate from gridplayer.utils.url_resolve.static import ResolvedVideo from gridplayer.utils.url_resolve.url_resolve import VideoURLResolver @@ -677,10 +677,8 @@ def loop_end_action(self): self.seek_random() else: self.seek(self.loop_start) - elif self.video_params.repeat_mode == VideoRepeat.DIR: + if not self.video_params.repeat_mode == VideoRepeat.SINGLE_FILE: self.next_video() - elif self.video_params.repeat_mode == VideoRepeat.DIR_SHUFFLE: - self.shuffle_video() def apply_snapshot(self, snapshot: Video): if snapshot.uri != self.video_params.uri: @@ -713,10 +711,25 @@ def apply_snapshot(self, snapshot: Video): def set_video(self, video_params: Video): is_first_video = self.video_params is None - is_options_changed = get_vlc_options(self.video_params) != get_vlc_options( - video_params - ) - + is_options_changed = get_vlc_options(self.video_params) != get_vlc_options(video_params) + + last_repeat_mode = getattr(self, 'last_repeat_mode', None) + is_repeat_mode_changed = last_repeat_mode != video_params.repeat_mode + + + if is_first_video or is_repeat_mode_changed: + self.original_dir = video_params.uri.parent # Use the parent directory of the first video + self.siblings = get_file_siblings(self.original_dir) # Compute sibling list for navigation + if video_params.repeat_mode == VideoRepeat.DIR_SHUFFLE: + self.navigator = VideoNavigator(video_params.uri, is_shuffle = True, is_recursive = False) + elif video_params.repeat_mode == VideoRepeat.REC_DIR_SHUFFLE: + self.navigator = VideoNavigator(video_params.uri, is_shuffle = True, is_recursive = True) + elif video_params.repeat_mode == VideoRepeat.REC_DIR: + self.navigator = VideoNavigator(video_params.uri, is_shuffle = False, is_recursive = True) + elif video_params.repeat_mode == VideoRepeat.DIR: + self.navigator = VideoNavigator(video_params.uri, is_shuffle = False, is_recursive = False) + + self.last_repeat_mode = video_params.repeat_mode self.video_params = video_params # Shut down current video @@ -1109,17 +1122,14 @@ def play_pause(self): @only_initialized @only_local_file def previous_video(self): - self.switch_video(previous_video_file(self.video_params.uri)) - + new_video = self.navigator.previous_video_file() + self.switch_video(new_video) + @only_initialized @only_local_file def next_video(self): - self.switch_video(next_video_file(self.video_params.uri)) - - @only_initialized - @only_local_file - def shuffle_video(self): - self.switch_video(next_video_file(self.video_params.uri, is_shuffle=True)) + new_video = self.navigator.next_video_file() + self.switch_video(new_video) @only_initialized @only_local_file