Improve performance of queue and playlist editing views with caching and curses tweaks

This commit is contained in:
mrkmntal 2026-10-27 13:04:51 -04:00
commit 44fac79997
2 changed files with 73 additions and 22 deletions

View file

@ -16,6 +16,10 @@ class App:
curses.curs_set(0)
stdscr.keypad(True)
stdscr.nodelay(False)
try:
curses.set_escdelay(50)
except (AttributeError, curses.error):
pass
self._init_colors()
@ -31,7 +35,8 @@ class App:
self.messages = []
# Video cache + background rescan
self._video_cache = []
self._video_cache = ()
self._video_cache_version = 0
self._video_cache_lock = threading.Lock()
self._rescan_stop = threading.Event()
self._scan_videos()
@ -60,8 +65,7 @@ class App:
try:
media_path = Path(config.MEDIA_DIR)
if not media_path.exists():
with self._video_cache_lock:
self._video_cache = []
self._commit_video_cache([])
return
videos = []
@ -70,11 +74,18 @@ class App:
rel = str(file_path.relative_to(media_path))
videos.append((rel, str(file_path)))
with self._video_cache_lock:
self._video_cache = sorted(videos, key=lambda x: x[0].lower())
self._commit_video_cache(videos)
except Exception:
pass
def _commit_video_cache(self, videos):
"""Publish an immutable, pre-normalized snapshot of scanned videos."""
sorted_videos = sorted(videos, key=lambda item: item[0].casefold())
cache = tuple((name, path, name.casefold()) for name, path in sorted_videos)
with self._video_cache_lock:
self._video_cache = cache
self._video_cache_version += 1
def _rescan_loop(self):
"""Background thread that rescans every 2 minutes."""
while not self._rescan_stop.is_set():
@ -88,7 +99,12 @@ class App:
def get_videos(self):
"""Return cached video list. Thread-safe."""
with self._video_cache_lock:
return self._video_cache[:]
return [(name, path) for name, path, _ in self._video_cache]
def get_videos_with_version(self):
"""Return an immutable normalized snapshot and its cache version."""
with self._video_cache_lock:
return self._video_cache, self._video_cache_version
def refresh_videos(self):
"""Rescan the configured media directory immediately."""

View file

@ -119,6 +119,7 @@ class MainView:
elif key == config.KEY_PLAYLIST:
self.app.switch_view('playlist')
elif key == config.KEY_QUEUE:
self.app.views['queue'].open()
self.app.switch_view('queue')
elif key == config.KEY_SETTINGS:
self.app.views['settings'].open()
@ -210,34 +211,65 @@ class QueueView:
self.selected = 0
self.scroll_offset = 0
self._last_search = None
self._last_cache_version = -1
self._indexed_videos = ()
self._all_videos = []
def open(self):
self._apply_filter()
def _apply_filter(self):
videos = self.app.get_videos()
if not self.search:
self.filtered = videos[:]
else:
s = self.search.lower()
self.filtered = [(r, a) for r, a in videos if s in r.lower()]
videos, cache_version = self.app.get_videos_with_version()
search_changed = self.search != self._last_search
cache_changed = cache_version != self._last_cache_version
if not search_changed and not cache_changed:
return
if self.search != self._last_search:
if cache_changed:
self._indexed_videos = videos
self._all_videos = [(name, path) for name, path, _ in videos]
self._last_cache_version = cache_version
if search_changed:
self.selected = 0
self.scroll_offset = 0
self._last_search = self.search
query = self.search.casefold()
if not query:
self.filtered = self._all_videos
else:
self.filtered = [
(name, path)
for name, path, normalized_name in self._indexed_videos
if query in normalized_name
]
self.selected = min(self.selected, max(0, len(self.filtered) - 1))
self._ensure_selection_visible()
def _ensure_selection_visible(self):
content_height = max(1, self.app.height - 3)
if self.selected < self.scroll_offset:
self.scroll_offset = self.selected
elif self.selected >= self.scroll_offset + content_height:
self.scroll_offset = self.selected - content_height + 1
max_scroll = max(0, len(self.filtered) - content_height)
self.scroll_offset = min(max(0, self.scroll_offset), max_scroll)
def draw(self):
self._apply_filter()
search_display = self.search if self.search else "(type to search)"
self.app.draw_status_bar(f" Queue Video | Search: {search_display}")
content_height = self.app.height - 3
if len(self.filtered) > content_height:
if self.selected < self.scroll_offset:
self.scroll_offset = self.selected
elif self.selected >= self.scroll_offset + content_height:
self.scroll_offset = self.selected - content_height + 1
content_height = max(1, self.app.height - 3)
scroll_offset = self.scroll_offset
if self.selected < scroll_offset:
scroll_offset = self.selected
elif self.selected >= scroll_offset + content_height:
scroll_offset = self.selected - content_height + 1
for i in range(content_height):
idx = self.scroll_offset + i
idx = scroll_offset + i
row = 2 + i
if row >= self.app.height - 1:
break
@ -262,6 +294,8 @@ class QueueView:
self.app.draw_help_bar(" [Type]Filter [Up/Down]Navigate [Enter]Queue [Esc]Back")
def handle_key(self, key):
# Check cheaply for a completed background scan before processing input.
self._apply_filter()
if key == 27: # Esc
if self.search:
self.search = ""
@ -271,7 +305,7 @@ class QueueView:
elif key == curses.KEY_UP:
self.selected = max(0, self.selected - 1)
elif key == curses.KEY_DOWN:
self.selected = min(len(self.filtered) - 1, self.selected + 1)
self.selected = min(max(0, len(self.filtered) - 1), self.selected + 1)
elif key in (10, 13): # Enter
if 0 <= self.selected < len(self.filtered):
_, abs_path = self.filtered[self.selected]
@ -286,6 +320,7 @@ class QueueView:
elif 32 <= key <= 126:
self.search += chr(key)
self._apply_filter()
self._ensure_selection_visible()
return True