Add settings view and configurable media directory for browsing

This commit is contained in:
mrkmntal 2026-10-27 12:53:32 -04:00
commit e12d36b103
4 changed files with 159 additions and 8 deletions

View file

@ -53,12 +53,13 @@ python3 -m mpvremote
## Configuration
All settings live in `mpvremote/config.py`:
The IPC socket and video extensions are set in `mpvremote/config.py`. The media
directory defaults to `~/Videos` and can be changed in the app's Settings view
(`x`). The selected path is stored in `~/.config/mpvremotemedia`.
| Setting | Default | Description |
|--------------|--------------------|---------------------------------------|
| `SOCKET` | `/tmp/mpv-socket` | Path to mpv's IPC socket |
| `MEDIA_DIR` | `~/GDrive/Videos` | Directory scanned for queueable video |
| `VIDEO_EXTS` | `mp4 mkv avi webm` | File extensions treated as video |
## Keybindings
@ -68,7 +69,7 @@ All settings live in `mpvremote/config.py`:
| Key | Action |
|---------|----------------------------|
| `Space` | Pause / resume |
| `h`/`l` | Seek -10s / +10s |
| `h`/`l`, `←`/`→` | Seek -10s / +10s |
| `j`/`k` | Volume -5 / +5 |
| `s` | Cycle subtitles |
| `n`/`p` | Next / previous track |
@ -76,6 +77,7 @@ All settings live in `mpvremote/config.py`:
| `R` | Toggle loop playlist |
| `e` | Playlist view |
| `o` | Queue view |
| `x` | Settings (media directory) |
| `c` | Show controls help |
| `q` | Quit |
@ -85,7 +87,7 @@ All settings live in `mpvremote/config.py`:
|----------------|--------------------------|
| `Up`/`Down` | Navigate |
| `Enter` | Skip to selected item |
| `Del` | Remove selected item |
| `Del`/`d` | Remove selected item |
| `Esc`/`q`/`←` | Back to main view |
### Queue view

View file

@ -4,7 +4,7 @@ import time
from pathlib import Path
from mpvremote import config
from mpvremote.mpv_client import MpvClient
from mpvremote.views import MainView, PlaylistView, QueueView
from mpvremote.views import MainView, PlaylistView, QueueView, SettingsView
class App:
@ -25,6 +25,7 @@ class App:
'main': MainView(self),
'playlist': PlaylistView(self),
'queue': QueueView(self),
'settings': SettingsView(self),
}
self.current_view = self.views['main']
self.messages = []
@ -89,6 +90,10 @@ class App:
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()

View file

@ -1,7 +1,32 @@
import os
from pathlib import Path
SOCKET = "/tmp/mpv-socket"
MEDIA_DIR = os.path.expanduser("~/GDrive/Videos")
DEFAULT_MEDIA_DIR = os.path.expanduser("~/Videos")
MEDIA_DIR_FILE = Path.home() / ".config" / "mpvremotemedia"
def _load_media_dir():
try:
configured_dir = MEDIA_DIR_FILE.read_text(encoding="utf-8").strip()
except (OSError, UnicodeError):
configured_dir = ""
return os.path.expanduser(configured_dir or DEFAULT_MEDIA_DIR)
def set_media_dir(path):
"""Persist and activate the configured media directory."""
path = path.strip()
if not path:
raise ValueError("Media directory cannot be empty")
MEDIA_DIR_FILE.parent.mkdir(parents=True, exist_ok=True)
MEDIA_DIR_FILE.write_text(path + "\n", encoding="utf-8")
global MEDIA_DIR
MEDIA_DIR = os.path.expanduser(path)
MEDIA_DIR = _load_media_dir()
VIDEO_EXTS = ["mp4", "mkv", "avi", "webm"]
# Key codes
@ -20,3 +45,4 @@ KEY_QUEUE = ord('o')
KEY_PLAYLIST = ord('e')
KEY_QUIT = ord('q')
KEY_DELETE_PLAYLIST = ord('d')
KEY_SETTINGS = ord('x')

View file

@ -57,7 +57,7 @@ class MainView:
self.app.draw_messages()
def _draw_help(self):
text = " [Space]Pause [KLeft/KRight]Seek [j/k]Vol [s]Sub [e]Plist [o]Queue [r/R]Loop [q]Quit"
text = " [Space]Pause [KLeft/KRight]Seek [j/k]Vol [s]Sub [e]Plist [o]Queue [x]Settings [r/R]Loop [q]Quit"
self.app.draw_help_bar(text)
def handle_key(self, key):
@ -114,12 +114,15 @@ class MainView:
elif key == config.KEY_CONTROLS:
self.app.add_message("=== Controls ===")
self.app.add_message("[Space] Pause | [KLeft/KRight] Seek -10s/+10s | [j/k] Vol -/+")
self.app.add_message("[s] Subtitle | [o] Queue | [e] Playlist | [n/p] Next/Prev")
self.app.add_message("[s] Subtitle | [o] Queue | [e] Playlist | [x] Settings | [n/p] Next/Prev")
self.app.add_message("[r/R] Loop File/Playlist (state shown in status bar) | [q] Quit")
elif key == config.KEY_PLAYLIST:
self.app.switch_view('playlist')
elif key == config.KEY_QUEUE:
self.app.switch_view('queue')
elif key == config.KEY_SETTINGS:
self.app.views['settings'].open()
self.app.switch_view('settings')
return True
@ -284,3 +287,118 @@ class QueueView:
self.search += chr(key)
self._apply_filter()
return True
class SettingsView:
def __init__(self, app):
self.app = app
self.path = config.MEDIA_DIR
self.cursor = len(self.path)
self.focus = 0 # 0: path field, 1: OK button
self.error = ""
def open(self):
self.path = config.MEDIA_DIR
self.cursor = len(self.path)
self.focus = 0
self.error = ""
def _put(self, row, col, text, attrs=0):
if row < 0 or row >= self.app.height or col >= self.app.width:
return
try:
self.app.stdscr.addstr(row, col, text[:max(0, self.app.width - col)], attrs)
except curses.error:
pass
def draw(self):
self.app.draw_status_bar(" Settings | Media Directory")
panel_width = max(1, min(72, self.app.width - 4))
panel_height = 9
left = max(0, (self.app.width - panel_width) // 2)
top = max(1, (self.app.height - panel_height) // 2)
inner_width = max(1, panel_width - 2)
self._put(top, left, "+" + "-" * max(0, panel_width - 2) + "+")
for row in range(top + 1, min(top + panel_height - 1, self.app.height - 1)):
self._put(row, left, "|" + " " * max(0, panel_width - 2) + "|")
self._put(top + panel_height - 1, left, "+" + "-" * max(0, panel_width - 2) + "+")
self._put(top + 1, left + 2, "Media directory (use Tab to focus OK):")
field_row = top + 3
field_col = left + 2
field_width = max(1, inner_width - 4)
cursor = self.cursor
start = max(0, cursor - field_width + 1)
visible_path = self.path[start:start + field_width]
field_attrs = curses.A_REVERSE if self.focus == 0 else 0
self._put(field_row, field_col, visible_path.ljust(field_width), field_attrs)
button = "[ OK ]"
button_col = left + max(2, (panel_width - len(button)) // 2)
button_attrs = curses.color_pair(2) | curses.A_BOLD if self.focus == 1 else 0
self._put(top + 5, button_col, button, button_attrs)
if self.error:
self._put(top + 6, left + 2, self.error[:inner_width], curses.color_pair(3))
self.app.draw_help_bar(" [Tab]Focus [Enter]Save [Esc]Cancel")
try:
if self.focus == 0:
curses.curs_set(1)
cursor_col = min(field_col + cursor - start, field_col + field_width - 1)
self.app.stdscr.move(field_row, cursor_col)
else:
curses.curs_set(0)
except curses.error:
pass
def _close(self):
try:
curses.curs_set(0)
except curses.error:
pass
self.app.switch_view('main')
def _save(self):
try:
config.set_media_dir(self.path)
except (OSError, ValueError) as exc:
self.error = f"Could not save directory: {exc}"
return
self.app.refresh_videos()
self.app.add_message(f">> Media directory set to {config.MEDIA_DIR}")
self._close()
def handle_key(self, key):
if key == 27:
self._close()
elif key in (9, curses.KEY_BTAB):
self.focus = 0 if key == curses.KEY_BTAB else 1 - self.focus
self.error = ""
elif self.focus == 1:
if key in (10, 13, curses.KEY_ENTER):
self._save()
elif key in (10, 13, curses.KEY_ENTER):
self.focus = 1
elif key in (curses.KEY_BACKSPACE, 127, 8):
if self.cursor:
self.path = self.path[:self.cursor - 1] + self.path[self.cursor:]
self.cursor -= 1
self.error = ""
elif key == curses.KEY_DC:
if self.cursor < len(self.path):
self.path = self.path[:self.cursor] + self.path[self.cursor + 1:]
self.error = ""
elif key == curses.KEY_LEFT:
self.cursor = max(0, self.cursor - 1)
elif key == curses.KEY_RIGHT:
self.cursor = min(len(self.path), self.cursor + 1)
elif key == curses.KEY_HOME:
self.cursor = 0
elif key == curses.KEY_END:
self.cursor = len(self.path)
elif 32 <= key <= 126:
self.path = self.path[:self.cursor] + chr(key) + self.path[self.cursor:]
self.cursor += 1
self.error = ""
return True