mpvremote/mpvremote/app.py

151 lines
4.7 KiB
Python

import curses
import threading
import time
from pathlib import Path
from mpvremote import config
from mpvremote.mpv_client import MpvClient
from mpvremote.views import MainView, PlaylistView, QueueView, SettingsView
class App:
def __init__(self, stdscr):
self.stdscr = stdscr
self.client = MpvClient(config.SOCKET)
self.running = True
curses.curs_set(0)
stdscr.keypad(True)
stdscr.nodelay(False)
self._init_colors()
self.height, self.width = stdscr.getmaxyx()
self.views = {
'main': MainView(self),
'playlist': PlaylistView(self),
'queue': QueueView(self),
'settings': SettingsView(self),
}
self.current_view = self.views['main']
self.messages = []
# Video cache + background rescan
self._video_cache = []
self._video_cache_lock = threading.Lock()
self._rescan_stop = threading.Event()
self._scan_videos()
self._rescan_thread = threading.Thread(target=self._rescan_loop, daemon=True)
self._rescan_thread.start()
def _init_colors(self):
curses.start_color()
curses.use_default_colors()
navy_bg = curses.COLOR_BLUE
if curses.can_change_color() and curses.COLORS >= 256:
try:
curses.init_color(16, 0, 0, 500)
navy_bg = 16
except curses.error:
pass
curses.init_pair(1, curses.COLOR_WHITE, navy_bg)
curses.init_pair(2, curses.COLOR_GREEN, -1)
curses.init_pair(3, curses.COLOR_YELLOW, -1)
curses.init_pair(4, curses.COLOR_WHITE, navy_bg)
def _scan_videos(self):
"""Scan MEDIA_DIR for video files. Thread-safe."""
try:
media_path = Path(config.MEDIA_DIR)
if not media_path.exists():
with self._video_cache_lock:
self._video_cache = []
return
videos = []
for ext in config.VIDEO_EXTS:
for file_path in media_path.rglob(f"*.{ext}"):
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())
except Exception:
pass
def _rescan_loop(self):
"""Background thread that rescans every 2 minutes."""
while not self._rescan_stop.is_set():
for _ in range(120):
if self._rescan_stop.is_set():
break
time.sleep(1)
if not self._rescan_stop.is_set():
self._scan_videos()
def get_videos(self):
"""Return cached video list. Thread-safe."""
with self._video_cache_lock:
return self._video_cache[:]
def refresh_videos(self):
"""Rescan the configured media directory immediately."""
self._scan_videos()
def run(self):
while self.running:
self.draw()
key = self.stdscr.getch()
if key == -1:
continue
if not self.current_view.handle_key(key):
self.running = False
self._rescan_stop.set()
self._rescan_thread.join(timeout=5)
def draw(self):
self.stdscr.erase()
self.current_view.draw()
self.stdscr.noutrefresh()
curses.doupdate()
def switch_view(self, name):
self.current_view = self.views[name]
self.stdscr.clear()
self.stdscr.refresh()
def add_message(self, msg):
self.messages.append(msg)
max_msgs = max(1, self.height - 4)
while len(self.messages) > max_msgs:
self.messages.pop(0)
def draw_status_bar(self, text):
text = text[:self.width - 1].ljust(self.width - 1)
try:
self.stdscr.addstr(0, 0, text, curses.color_pair(1) | curses.A_BOLD)
except curses.error:
pass
def draw_help_bar(self, text):
row = self.height - 1
text = text[:self.width - 1].ljust(self.width - 1)
try:
self.stdscr.addstr(row, 0, text, curses.color_pair(4))
except curses.error:
pass
def draw_messages(self, start_row=2):
max_rows = max(0, self.height - start_row - 1)
visible = self.messages[-max_rows:] if max_rows > 0 else []
for i, msg in enumerate(visible):
row = start_row + i
if row >= self.height - 1:
break
text = msg[:self.width - 1]
try:
self.stdscr.addstr(row, 0, text, curses.color_pair(3))
except curses.error:
pass