enkan 2.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- enkan/__init__.py +0 -0
- enkan/__main__.py +43 -0
- enkan/cache/HistoryManager.py +71 -0
- enkan/cache/ImageCacheManager.py +203 -0
- enkan/cache/LRUCache.py +54 -0
- enkan/cache/PreloadQueue.py +73 -0
- enkan/cache/__init__.py +0 -0
- enkan/cli.py +90 -0
- enkan/constants.py +33 -0
- enkan/mySlideshow/ProviderChoooseDialog.py +56 -0
- enkan/mySlideshow/ZoomPan.py +323 -0
- enkan/mySlideshow/__init__.py +0 -0
- enkan/mySlideshow/mySlideshow.py +858 -0
- enkan/mySlideshow/start_slideshow.py +31 -0
- enkan/plugables/ImageLoaders.py +63 -0
- enkan/plugables/ImageProviders.py +169 -0
- enkan/plugables/__init__.py +0 -0
- enkan/tree/Grafting.py +128 -0
- enkan/tree/Tree.py +264 -0
- enkan/tree/TreeBuilder.py +379 -0
- enkan/tree/TreeNode.py +80 -0
- enkan/tree/__init__.py +0 -0
- enkan/tree/tree_logic.py +258 -0
- enkan/utils/Defaults.py +151 -0
- enkan/utils/Filters.py +80 -0
- enkan/utils/InputProcessor.py +335 -0
- enkan/utils/__init__.py +0 -0
- enkan/utils/argparse_setup.py +114 -0
- enkan/utils/logging.py +17 -0
- enkan/utils/myStack.py +63 -0
- enkan/utils/tests.py +204 -0
- enkan/utils/utils.py +342 -0
- enkan-2.0.2.dist-info/METADATA +12 -0
- enkan-2.0.2.dist-info/RECORD +38 -0
- enkan-2.0.2.dist-info/WHEEL +5 -0
- enkan-2.0.2.dist-info/entry_points.txt +2 -0
- enkan-2.0.2.dist-info/licenses/LICENSE +21 -0
- enkan-2.0.2.dist-info/top_level.txt +1 -0
enkan/__init__.py
ADDED
|
File without changes
|
enkan/__main__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# # A Python script to create a slideshow from a list of files, with various
|
|
3
|
+
# modes and features including weighted, balanced, and random selection.
|
|
4
|
+
#
|
|
5
|
+
# Author: John Sullivan
|
|
6
|
+
# Created: 2023-2025
|
|
7
|
+
# License: MIT License
|
|
8
|
+
# Copyright: (c) 2023-2025 John Sullivan
|
|
9
|
+
#
|
|
10
|
+
# Description:
|
|
11
|
+
# This script builds a slideshow from directories or lists of images,
|
|
12
|
+
# supporting advanced weighting, folder balancing, and navigation features.
|
|
13
|
+
# It uses Tkinter for the GUI and Pillow for image handling.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# enkan --input_file <file_or_folder> [options]
|
|
17
|
+
#
|
|
18
|
+
# Dependencies:
|
|
19
|
+
# - Python 3.8+
|
|
20
|
+
# - Pillow
|
|
21
|
+
# - tqdm
|
|
22
|
+
#
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from enkan.utils.argparse_setup import get_arg_parser
|
|
27
|
+
from enkan.utils.logging import configure_logging
|
|
28
|
+
from enkan.cli import main_with_args
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
"""Console script / module entry point."""
|
|
32
|
+
args = get_arg_parser().parse_args()
|
|
33
|
+
configure_logging(debug=getattr(args, "debug", False), quiet=getattr(args, "quiet", False))
|
|
34
|
+
try:
|
|
35
|
+
main_with_args(args)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logging.getLogger("enkan").error("Fatal: %s", e)
|
|
38
|
+
# No traceback shown unless debug enabled
|
|
39
|
+
if getattr(args, "debug", False):
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from enkan import constants
|
|
3
|
+
|
|
4
|
+
class HistoryManager:
|
|
5
|
+
"""
|
|
6
|
+
A single-line history manager with back/forward navigation.
|
|
7
|
+
Maintains a deque of visited paths with a current index pointer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, max_length=constants.HISTORY_QUEUE_LENGTH):
|
|
11
|
+
self.history = deque(maxlen=max_length)
|
|
12
|
+
self.current_index = -1 # -1 = no current item
|
|
13
|
+
self.max_length = max_length
|
|
14
|
+
|
|
15
|
+
def add(self, path):
|
|
16
|
+
"""
|
|
17
|
+
Add a new path. Clears forward history if the user
|
|
18
|
+
has navigated backward and is now moving forward naturally.
|
|
19
|
+
"""
|
|
20
|
+
# Remove everything after the current index (future)
|
|
21
|
+
while len(self.history) - 1 > self.current_index:
|
|
22
|
+
self.history.pop()
|
|
23
|
+
|
|
24
|
+
# Add new path
|
|
25
|
+
self.history.append(path)
|
|
26
|
+
self.current_index = len(self.history) - 1
|
|
27
|
+
|
|
28
|
+
def remove(self, path):
|
|
29
|
+
"""
|
|
30
|
+
Remove a path from history, adjusting current_index if needed.
|
|
31
|
+
If the current item is removed, current_index is moved to the previous item.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
idx = self.history.index(path)
|
|
35
|
+
except ValueError:
|
|
36
|
+
return # Path not in history
|
|
37
|
+
|
|
38
|
+
self.history.remove(path)
|
|
39
|
+
|
|
40
|
+
# Adjust current_index
|
|
41
|
+
if idx < self.current_index:
|
|
42
|
+
self.current_index -= 1
|
|
43
|
+
elif idx == self.current_index:
|
|
44
|
+
# Move to the previous item, or -1 if none exist
|
|
45
|
+
self.current_index = max(0, self.current_index - 1) if self.history else -1
|
|
46
|
+
|
|
47
|
+
def back(self):
|
|
48
|
+
if self.current_index > 0:
|
|
49
|
+
self.current_index -= 1
|
|
50
|
+
return self.history[self.current_index]
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def forward_step(self):
|
|
54
|
+
if self.current_index < len(self.history) - 1:
|
|
55
|
+
self.current_index += 1
|
|
56
|
+
return self.history[self.current_index]
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def current(self):
|
|
60
|
+
"""Return the current item (or None if empty)."""
|
|
61
|
+
if self.current_index == -1:
|
|
62
|
+
return None
|
|
63
|
+
return self.history[self.current_index]
|
|
64
|
+
|
|
65
|
+
def clear(self):
|
|
66
|
+
self.history.clear()
|
|
67
|
+
self.current_index = -1
|
|
68
|
+
|
|
69
|
+
def __repr__(self):
|
|
70
|
+
line = list(self.history)
|
|
71
|
+
return f"HistoryManager(line={line}, current_index={self.current_index})"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from PIL import Image
|
|
5
|
+
|
|
6
|
+
from .LRUCache import LRUCache
|
|
7
|
+
from .PreloadQueue import PreloadQueue
|
|
8
|
+
from .HistoryManager import HistoryManager
|
|
9
|
+
from enkan.plugables.ImageLoaders import ImageLoaders
|
|
10
|
+
from enkan.utils.utils import is_videofile
|
|
11
|
+
from enkan import constants
|
|
12
|
+
|
|
13
|
+
logger: logging.Logger = logging.getLogger("enkan.cache")
|
|
14
|
+
|
|
15
|
+
class ImageCacheManager:
|
|
16
|
+
"""
|
|
17
|
+
Combines LRUCache, PreloadQueue, and HistoryManager
|
|
18
|
+
to manage images and videos for the slideshow.
|
|
19
|
+
Supports background preloading of images.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, image_provider, current_image_index, background_preload=True):
|
|
23
|
+
self.lru_cache = LRUCache(constants.CACHE_SIZE)
|
|
24
|
+
self.preload_queue = PreloadQueue(constants.PRELOAD_QUEUE_LENGTH)
|
|
25
|
+
self.history_manager = HistoryManager(constants.HISTORY_QUEUE_LENGTH)
|
|
26
|
+
self.image_provider = image_provider
|
|
27
|
+
self.image_loader = ImageLoaders()
|
|
28
|
+
self.current_image_index = current_image_index
|
|
29
|
+
self.background_preload = background_preload
|
|
30
|
+
|
|
31
|
+
self._lock = threading.Lock()
|
|
32
|
+
self._refill_thread = None
|
|
33
|
+
self._provider_lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
# Initial preload (async if background=True)
|
|
36
|
+
if self.background_preload:
|
|
37
|
+
self._background_refill()
|
|
38
|
+
else:
|
|
39
|
+
self._preload_refill()
|
|
40
|
+
|
|
41
|
+
# -----------------------
|
|
42
|
+
# Preload
|
|
43
|
+
# -----------------------
|
|
44
|
+
|
|
45
|
+
def _provider_next(self) -> str:
|
|
46
|
+
"""Thread-safe wrapper around the image provider."""
|
|
47
|
+
with self._provider_lock:
|
|
48
|
+
return next(self.image_provider)
|
|
49
|
+
|
|
50
|
+
def _preload_refill(self) -> None:
|
|
51
|
+
"""Fill preload queue fully."""
|
|
52
|
+
while len(self.preload_queue) < self.preload_queue.max_size:
|
|
53
|
+
try:
|
|
54
|
+
image_path: str = self._provider_next()
|
|
55
|
+
except StopIteration:
|
|
56
|
+
break # For linear/sequential provider; random providers never stop
|
|
57
|
+
if image_path is None:
|
|
58
|
+
break
|
|
59
|
+
if image_path in self.preload_queue:
|
|
60
|
+
continue
|
|
61
|
+
image_obj: Image.Image = self._load_image(image_path)
|
|
62
|
+
self.preload_queue.push(image_path, image_obj)
|
|
63
|
+
logger.debug(f"Preloaded: {image_path}")
|
|
64
|
+
|
|
65
|
+
def _background_refill(self):
|
|
66
|
+
"""Start a background refill thread if not already running."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
if self._refill_thread and self._refill_thread.is_alive():
|
|
69
|
+
logger.debug("Background refill aborted. Already refilling.")
|
|
70
|
+
return # Already refilling
|
|
71
|
+
self._refill_thread = threading.Thread(
|
|
72
|
+
target=self._preload_refill, daemon=True
|
|
73
|
+
)
|
|
74
|
+
self._refill_thread.start()
|
|
75
|
+
logger.debug("Background refill started.")
|
|
76
|
+
|
|
77
|
+
# -----------------------
|
|
78
|
+
# Image Loading
|
|
79
|
+
# -----------------------
|
|
80
|
+
|
|
81
|
+
def _load_image(self, image_path: str) -> Image.Image | None:
|
|
82
|
+
if is_videofile(image_path):
|
|
83
|
+
return None
|
|
84
|
+
image_obj: Image.Image = self.lru_cache.get(image_path)
|
|
85
|
+
if image_obj is None:
|
|
86
|
+
logger.debug(f"Loading from Disk: {image_path}")
|
|
87
|
+
image_obj: Image.Image = self.image_loader.load_image(image_path)
|
|
88
|
+
self.lru_cache.put(image_path, image_obj)
|
|
89
|
+
else:
|
|
90
|
+
logger.debug(f"Loaded from LRUCache: {image_path}")
|
|
91
|
+
return image_obj
|
|
92
|
+
|
|
93
|
+
# -----------------------
|
|
94
|
+
# Public API
|
|
95
|
+
# -----------------------
|
|
96
|
+
|
|
97
|
+
def get_next(
|
|
98
|
+
self,
|
|
99
|
+
image_path: str = None,
|
|
100
|
+
record_history: bool = True
|
|
101
|
+
) -> tuple[str | None, Image.Image | None]:
|
|
102
|
+
"""
|
|
103
|
+
Retrieve the next (image_path, image_obj).
|
|
104
|
+
If image_path is None, pop from PreloadQueue.
|
|
105
|
+
Updates history if record_history=True.
|
|
106
|
+
"""
|
|
107
|
+
image_obj: Image.Image = None
|
|
108
|
+
source: str = None
|
|
109
|
+
|
|
110
|
+
# Step 1: Pop from preload if no explicit path is given
|
|
111
|
+
if image_path is None:
|
|
112
|
+
popped = self.preload_queue.pop()
|
|
113
|
+
if popped:
|
|
114
|
+
image_path, image_obj = next(iter(popped.items()))
|
|
115
|
+
source = "PreloadQueue"
|
|
116
|
+
else:
|
|
117
|
+
try:
|
|
118
|
+
image_path = self._provider_next()
|
|
119
|
+
except StopIteration:
|
|
120
|
+
logger.debug("No image provided (empty provider).")
|
|
121
|
+
return None, None
|
|
122
|
+
image_obj = self._load_image(image_path)
|
|
123
|
+
source = "Disk"
|
|
124
|
+
else:
|
|
125
|
+
image_obj = self.lru_cache.get(image_path)
|
|
126
|
+
if image_obj:
|
|
127
|
+
source = "LRUCache"
|
|
128
|
+
else:
|
|
129
|
+
image_obj = self._load_image(image_path)
|
|
130
|
+
source = "Disk"
|
|
131
|
+
|
|
132
|
+
# Step 2: Conditionally update history
|
|
133
|
+
if record_history:
|
|
134
|
+
self.history_manager.add(image_path)
|
|
135
|
+
|
|
136
|
+
# Step 3: Refill preload (in background if enabled)
|
|
137
|
+
if self.background_preload:
|
|
138
|
+
self._background_refill()
|
|
139
|
+
else:
|
|
140
|
+
self._preload_refill()
|
|
141
|
+
|
|
142
|
+
# Step 4: Debug output
|
|
143
|
+
logger.debug(f"Loaded: {image_path} (from {source})")
|
|
144
|
+
|
|
145
|
+
return image_path, image_obj
|
|
146
|
+
|
|
147
|
+
def back(self):
|
|
148
|
+
path = self.history_manager.back()
|
|
149
|
+
if path is None:
|
|
150
|
+
return None, None
|
|
151
|
+
return path, self._load_image(path)
|
|
152
|
+
|
|
153
|
+
def forward(self):
|
|
154
|
+
path = self.history_manager.forward_step()
|
|
155
|
+
if path is None:
|
|
156
|
+
return None, None
|
|
157
|
+
return path, self._load_image(path)
|
|
158
|
+
|
|
159
|
+
def current(self):
|
|
160
|
+
path = self.history_manager.current()
|
|
161
|
+
if path is None:
|
|
162
|
+
return None, None
|
|
163
|
+
return path, self._load_image(path)
|
|
164
|
+
|
|
165
|
+
def reset(self):
|
|
166
|
+
"""Clear all caches and history."""
|
|
167
|
+
self.lru_cache.clear()
|
|
168
|
+
self.preload_queue.clear()
|
|
169
|
+
self.history_manager.clear()
|
|
170
|
+
logger.debug("Cache, preload queue, and history cleared.")
|
|
171
|
+
|
|
172
|
+
if self.background_preload:
|
|
173
|
+
self._background_refill()
|
|
174
|
+
else:
|
|
175
|
+
self._preload_refill()
|
|
176
|
+
|
|
177
|
+
def reset_provider(self) -> bool:
|
|
178
|
+
"""Reset provider-specific state and clear pending preloads."""
|
|
179
|
+
reset_callable = getattr(self.image_provider, "reset_burst", None)
|
|
180
|
+
if not callable(reset_callable):
|
|
181
|
+
return False
|
|
182
|
+
with self._provider_lock:
|
|
183
|
+
reset_callable()
|
|
184
|
+
self.preload_queue.clear()
|
|
185
|
+
if self.background_preload:
|
|
186
|
+
self._background_refill()
|
|
187
|
+
else:
|
|
188
|
+
self._preload_refill()
|
|
189
|
+
logger.debug("Provider state reset; preload queue cleared.")
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def invalidate(self, image_path: str) -> None:
|
|
193
|
+
"""Remove a specific path from caches and preload queue."""
|
|
194
|
+
self.lru_cache.pop(image_path)
|
|
195
|
+
self.preload_queue.discard(image_path)
|
|
196
|
+
|
|
197
|
+
def __repr__(self):
|
|
198
|
+
return (
|
|
199
|
+
f"ImageCacheManager("
|
|
200
|
+
f"cache={self.lru_cache}, "
|
|
201
|
+
f"preload={self.preload_queue}, "
|
|
202
|
+
f"history={self.history_manager})"
|
|
203
|
+
)
|
enkan/cache/LRUCache.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
|
|
3
|
+
class LRUCache:
|
|
4
|
+
"""
|
|
5
|
+
A simple LRU (Least Recently Used) cache implemented using OrderedDict.
|
|
6
|
+
Keys are promoted to the front on access or insertion.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, max_size: int):
|
|
10
|
+
self.cache = OrderedDict()
|
|
11
|
+
self.max_size = max_size
|
|
12
|
+
|
|
13
|
+
def put(self, key, value):
|
|
14
|
+
if key in self.cache:
|
|
15
|
+
# Update and promote
|
|
16
|
+
self.cache.move_to_end(key, last=False)
|
|
17
|
+
self.cache[key] = value
|
|
18
|
+
else:
|
|
19
|
+
self.cache[key] = value
|
|
20
|
+
self.cache.move_to_end(key, last=False)
|
|
21
|
+
if len(self.cache) > self.max_size:
|
|
22
|
+
self.cache.popitem(last=True) # Remove oldest
|
|
23
|
+
|
|
24
|
+
def get(self, key):
|
|
25
|
+
if key not in self.cache:
|
|
26
|
+
return None
|
|
27
|
+
self.cache.move_to_end(key, last=False)
|
|
28
|
+
return self.cache[key]
|
|
29
|
+
|
|
30
|
+
def pop(self, key):
|
|
31
|
+
return self.cache.pop(key, None)
|
|
32
|
+
|
|
33
|
+
def clear(self):
|
|
34
|
+
"""Remove all items from the cache."""
|
|
35
|
+
self.cache.clear()
|
|
36
|
+
|
|
37
|
+
def __contains__(self, key):
|
|
38
|
+
return key in self.cache
|
|
39
|
+
|
|
40
|
+
def __getitem__(self, key):
|
|
41
|
+
return self.get(key)
|
|
42
|
+
|
|
43
|
+
def __setitem__(self, key, value):
|
|
44
|
+
self.put(key, value)
|
|
45
|
+
|
|
46
|
+
def items(self):
|
|
47
|
+
"""Return items in most-recently-used order."""
|
|
48
|
+
return list(self.cache.items())
|
|
49
|
+
|
|
50
|
+
def __len__(self):
|
|
51
|
+
return len(self.cache)
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return f"LRUCache({list(self.cache.items())})"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
|
|
3
|
+
class PreloadQueue:
|
|
4
|
+
"""
|
|
5
|
+
A FIFO queue of preloaded images with a maximum size.
|
|
6
|
+
Each element is a {path: image_obj} dict.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, max_size: int):
|
|
10
|
+
self.queue = deque(maxlen=max_size)
|
|
11
|
+
self.lookup = set() # For fast membership checks
|
|
12
|
+
self.max_size = max_size
|
|
13
|
+
|
|
14
|
+
def push(self, path, image_obj):
|
|
15
|
+
"""
|
|
16
|
+
Push a new (path, image_obj) to the bottom of the queue.
|
|
17
|
+
If the queue is full, the oldest item is automatically dropped.
|
|
18
|
+
"""
|
|
19
|
+
if path in self.lookup:
|
|
20
|
+
return False # Avoid duplicates
|
|
21
|
+
|
|
22
|
+
# If queue is full, remove oldest
|
|
23
|
+
if len(self.queue) == self.max_size:
|
|
24
|
+
oldest = self.queue.popleft()
|
|
25
|
+
self.lookup.difference_update(oldest.keys())
|
|
26
|
+
|
|
27
|
+
self.queue.append({path: image_obj})
|
|
28
|
+
self.lookup.add(path)
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
def pop(self):
|
|
32
|
+
"""
|
|
33
|
+
Pop and return the top (oldest) item in the queue.
|
|
34
|
+
Returns None if the queue is empty.
|
|
35
|
+
"""
|
|
36
|
+
if not self.queue:
|
|
37
|
+
return None
|
|
38
|
+
item = self.queue.popleft()
|
|
39
|
+
self.lookup.difference_update(item.keys())
|
|
40
|
+
return item
|
|
41
|
+
|
|
42
|
+
def discard(self, path):
|
|
43
|
+
"""
|
|
44
|
+
Remove a specific path from the queue if present.
|
|
45
|
+
Returns True if the path was removed, else False.
|
|
46
|
+
"""
|
|
47
|
+
if path not in self.lookup:
|
|
48
|
+
return False
|
|
49
|
+
self.lookup.discard(path)
|
|
50
|
+
new_queue = deque(maxlen=self.max_size)
|
|
51
|
+
for item in self.queue:
|
|
52
|
+
if path not in item:
|
|
53
|
+
new_queue.append(item)
|
|
54
|
+
self.queue = new_queue
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def clear(self):
|
|
58
|
+
"""Remove all items from the queue."""
|
|
59
|
+
self.queue.clear()
|
|
60
|
+
self.lookup.clear()
|
|
61
|
+
|
|
62
|
+
def __contains__(self, path):
|
|
63
|
+
return path in self.lookup
|
|
64
|
+
|
|
65
|
+
def __len__(self):
|
|
66
|
+
return len(self.queue)
|
|
67
|
+
|
|
68
|
+
def items(self):
|
|
69
|
+
"""Return a list of all queued items in order."""
|
|
70
|
+
return list(self.queue)
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return f"PreloadQueue({list(self.queue)})"
|
enkan/cache/__init__.py
ADDED
|
File without changes
|
enkan/cli.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ——— Standard library ———
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List
|
|
5
|
+
from itertools import accumulate
|
|
6
|
+
|
|
7
|
+
# ——— Local ———
|
|
8
|
+
from enkan.utils.InputProcessor import InputProcessor
|
|
9
|
+
from enkan.utils.Defaults import Defaults
|
|
10
|
+
from enkan.utils.Filters import Filters
|
|
11
|
+
from enkan.tree.tree_logic import (
|
|
12
|
+
build_tree,
|
|
13
|
+
extract_image_paths_and_weights_from_tree
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("enkan.main")
|
|
17
|
+
|
|
18
|
+
def main_with_args(args) -> None:
|
|
19
|
+
|
|
20
|
+
defaults: Defaults = Defaults(args=args)
|
|
21
|
+
filters: Filters = Filters()
|
|
22
|
+
filters.preprocess_ignored_files()
|
|
23
|
+
tree: Tree = None
|
|
24
|
+
|
|
25
|
+
# Normalize input_files to list
|
|
26
|
+
if not args.input_file:
|
|
27
|
+
input_files: List[str] = []
|
|
28
|
+
elif isinstance(args.input_file, str):
|
|
29
|
+
input_files = [args.input_file]
|
|
30
|
+
else:
|
|
31
|
+
input_files = list(args.input_file)
|
|
32
|
+
|
|
33
|
+
processor = InputProcessor(defaults, filters, args.quiet or False)
|
|
34
|
+
tree, image_dirs, specific_images, all_images, weights = processor.process_inputs(input_files)
|
|
35
|
+
if getattr(args, "ignore_below_bottom", False):
|
|
36
|
+
filters.configure_ignore_below_bottom(True, defaults.mode)
|
|
37
|
+
|
|
38
|
+
if not any([tree, image_dirs, specific_images, all_images, weights]):
|
|
39
|
+
raise ValueError("No images found in the provided input files.")
|
|
40
|
+
|
|
41
|
+
if image_dirs or specific_images or tree:
|
|
42
|
+
# Instantiate and build the tree
|
|
43
|
+
if not tree:
|
|
44
|
+
tree = build_tree(defaults, filters, image_dirs, specific_images, quiet=False)
|
|
45
|
+
|
|
46
|
+
# Print tree if requested
|
|
47
|
+
if args.printtree:
|
|
48
|
+
from enkan.utils.tests import print_tree
|
|
49
|
+
print_tree(defaults, tree.root, max_depth=args.testdepth or 9999)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if args.outputtree:
|
|
53
|
+
from enkan.utils.utils import write_tree_to_file
|
|
54
|
+
base_names = [os.path.splitext(os.path.basename(f))[0] for f in args.input_file]
|
|
55
|
+
output_name = "_".join(base_names) + ".tree"
|
|
56
|
+
output_path = os.path.join(os.getcwd(), output_name)
|
|
57
|
+
write_tree_to_file(tree, output_path)
|
|
58
|
+
logger.info("Tree written to %s", output_path)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Extract paths and weights from the tree
|
|
62
|
+
extracted_images, extracted_weights = (
|
|
63
|
+
extract_image_paths_and_weights_from_tree(tree, test_iterations=args.test)
|
|
64
|
+
)
|
|
65
|
+
all_images.extend(extracted_images)
|
|
66
|
+
weights.extend(extracted_weights)
|
|
67
|
+
|
|
68
|
+
cum_weights = list(accumulate(weights))
|
|
69
|
+
|
|
70
|
+
if args.outputlist:
|
|
71
|
+
from enkan.utils.utils import write_image_list
|
|
72
|
+
# Build output filename
|
|
73
|
+
base_names = [os.path.splitext(os.path.basename(f))[0] for f in args.input_file]
|
|
74
|
+
output_name = "_".join(base_names) + ".lst"
|
|
75
|
+
output_path = os.path.join(os.getcwd(), output_name)
|
|
76
|
+
write_image_list(all_images, weights, args.input_file, args.mode, output_path)
|
|
77
|
+
logger.info("Output written to %s", output_path)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Test or start the slideshow
|
|
81
|
+
if args.test:
|
|
82
|
+
from enkan.utils.tests import test_distribution
|
|
83
|
+
test_distribution(all_images, cum_weights, args.test, args.testdepth, args.histo, defaults, args.quiet or False)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
from enkan.mySlideshow.start_slideshow import start_slideshow
|
|
87
|
+
if not tree:
|
|
88
|
+
from enkan.tree.Tree import Tree
|
|
89
|
+
tree = Tree(defaults, filters)
|
|
90
|
+
start_slideshow(tree, all_images, cum_weights, defaults, filters, args.quiet or False, args.interval)
|
enkan/constants.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
3
|
+
|
|
4
|
+
PACKAGE_NAME = "enkan"
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
VERSION = _pkg_version(PACKAGE_NAME)
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
# Fallback during source-only situations (keep in sync with pyproject if used)
|
|
10
|
+
VERSION = "2.0.2"
|
|
11
|
+
TOTAL_WEIGHT = 100
|
|
12
|
+
PARENT_STACK_MAX = 5
|
|
13
|
+
HISTORY_QUEUE_LENGTH = 25
|
|
14
|
+
CACHE_SIZE = 10
|
|
15
|
+
PRELOAD_QUEUE_LENGTH = 3
|
|
16
|
+
IMAGE_FILES = (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff")
|
|
17
|
+
VIDEO_FILES = (".mp4", ".mkv", ".webm", ".avi", ".mov", ".wmv")
|
|
18
|
+
TEXT_FILES = (".txt", ".lst")
|
|
19
|
+
CX_PATTERN = re.compile(r"^(?:[bw]\d+(?:,-?\d+)?(?:,-?\d+)?)+$", re.IGNORECASE)
|
|
20
|
+
|
|
21
|
+
MODIFIER_PATTERN = re.compile(r"(\[.*?\])")
|
|
22
|
+
WEIGHT_MODIFIER_PATTERN = re.compile(r"^\d+%?$")
|
|
23
|
+
PROPORTION_PATTERN = re.compile(r"^%\d+%?$")
|
|
24
|
+
MODE_PATTERN = CX_PATTERN
|
|
25
|
+
GRAFT_PATTERN = re.compile(r"^g\d+$", re.IGNORECASE)
|
|
26
|
+
GROUP_PATTERN = re.compile(r"^>.*", re.IGNORECASE)
|
|
27
|
+
FLAT_PATTERN = re.compile(r"^f$", re.IGNORECASE)
|
|
28
|
+
VIDEO_PATTERN = re.compile(r"^v$", re.IGNORECASE)
|
|
29
|
+
NO_VIDEO_PATTERN = re.compile(r"^nv$", re.IGNORECASE)
|
|
30
|
+
MUTE_PATTERN = re.compile(r"^m$", re.IGNORECASE)
|
|
31
|
+
NO_MUTE_PATTERN = re.compile(r"^nm$", re.IGNORECASE)
|
|
32
|
+
DONT_RECURSE_PATTERN = re.compile(r"^/$", re.IGNORECASE)
|
|
33
|
+
IGNORE_BELOW_BOTTOM_PATTERN = re.compile(r"^ibb$", re.IGNORECASE)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import simpledialog, messagebox
|
|
3
|
+
|
|
4
|
+
class ProviderChooserDialog(simpledialog.Dialog):
|
|
5
|
+
def __init__(self, parent, provider_names, title="Choose Image Provider"):
|
|
6
|
+
self.provider_names = provider_names
|
|
7
|
+
self.selected_provider = provider_names[0]
|
|
8
|
+
self.extra_kwargs = {}
|
|
9
|
+
super().__init__(parent, title)
|
|
10
|
+
|
|
11
|
+
def body(self, master):
|
|
12
|
+
tk.Label(master, text="Select image provider:").pack(padx=10, pady=10)
|
|
13
|
+
self.var = tk.StringVar(value=self.provider_names[0])
|
|
14
|
+
self.dropdown = tk.OptionMenu(master, self.var, *self.provider_names, command=self.on_provider_change)
|
|
15
|
+
self.dropdown.pack(padx=10, pady=(0, 10))
|
|
16
|
+
# Placeholder for extra arg widgets
|
|
17
|
+
self.extra_frame = tk.Frame(master)
|
|
18
|
+
self.extra_frame.pack()
|
|
19
|
+
self.weight_entry = None
|
|
20
|
+
self.on_provider_change(self.provider_names[0])
|
|
21
|
+
return self.dropdown
|
|
22
|
+
|
|
23
|
+
def on_provider_change(self, selected):
|
|
24
|
+
# Remove any extra fields
|
|
25
|
+
for widget in self.extra_frame.winfo_children():
|
|
26
|
+
widget.destroy()
|
|
27
|
+
self.extra_kwargs.clear()
|
|
28
|
+
|
|
29
|
+
if selected == "weighted":
|
|
30
|
+
tk.Label(self.extra_frame, text="Weights (comma-separated):").pack(side=tk.LEFT)
|
|
31
|
+
self.weight_entry = tk.Entry(self.extra_frame)
|
|
32
|
+
self.weight_entry.pack(side=tk.LEFT)
|
|
33
|
+
|
|
34
|
+
def apply(self):
|
|
35
|
+
self.selected_provider = self.var.get()
|
|
36
|
+
if self.selected_provider == "weighted" and self.weight_entry:
|
|
37
|
+
weights_text = self.weight_entry.get()
|
|
38
|
+
try:
|
|
39
|
+
weights = [float(w.strip()) for w in weights_text.split(',') if w.strip()]
|
|
40
|
+
self.extra_kwargs['weights'] = weights
|
|
41
|
+
except Exception:
|
|
42
|
+
messagebox.showerror("Invalid Weights", "Weights must be a comma-separated list of numbers.")
|
|
43
|
+
self.extra_kwargs['weights'] = []
|
|
44
|
+
# Add more provider-specific arguments as needed
|
|
45
|
+
|
|
46
|
+
def choose_and_set_provider(root, providers_instance, image_paths):
|
|
47
|
+
provider_names = list(providers_instance.providers.keys())
|
|
48
|
+
dlg = ProviderChooserDialog(root, provider_names)
|
|
49
|
+
chosen = dlg.selected_provider
|
|
50
|
+
kwargs = dlg.extra_kwargs
|
|
51
|
+
if chosen:
|
|
52
|
+
providers_instance.reset_manager(image_paths, provider_name=chosen, **kwargs)
|
|
53
|
+
tk.messagebox.showinfo("Provider Changed", f"Provider switched to: {chosen}")
|
|
54
|
+
|
|
55
|
+
# Usage remains:
|
|
56
|
+
# root.bind('<p>', lambda event: choose_and_set_provider(root, providers, image_paths))
|