Add settings view and configurable media directory for browsing
This commit is contained in:
parent
0065141467
commit
e12d36b103
4 changed files with 159 additions and 8 deletions
10
README.md
10
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue