Improve performance of queue and playlist editing views with caching and curses tweaks
This commit is contained in:
parent
e12d36b103
commit
44fac79997
2 changed files with 73 additions and 22 deletions
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue