feat: configurable media player selection with auto-detection
Backend:
- Add mediahive/players.py with cross-platform player detection:
- Windows: MPC-BE, MPC-HC, VLC, PotPlayer, mpv (registry + known paths)
- macOS: IINA, VLC (/Applications bundle scanning)
- Linux: VLC, SMPlayer (PATH via which)
- Add GET /api/players endpoint returning detected players
- Extend POST /api/roots/{id}/play to accept player_id and player_custom_cmd
- Make MPC-BE web UI port configurable via ?port= query param
Frontend:
- Add player selection to settings (localStorage, per-client)
- Two-column layout: player list (9.5em) + contextual options panel
- Contextual options: Custom Command input, MPC Web UI Port input
- Empty MPC port disables all Web UI polling/connection attempts
- MPC-BE polling only runs when MPC family player is selected and port is set
This commit is contained in:
@@ -142,6 +142,66 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section-title">Player</h2>
|
||||
<p class="settings-section-desc">Choose which media player to launch files with.</p>
|
||||
|
||||
<div class="player-layout">
|
||||
<div class="player-list">
|
||||
<label
|
||||
v-for="player in detectedPlayers"
|
||||
:key="player.id"
|
||||
class="player-radio-label"
|
||||
>
|
||||
<input
|
||||
class="player-radio"
|
||||
type="radio"
|
||||
name="player-selection"
|
||||
:checked="settings.playerId === player.id"
|
||||
@change="setPlayer(player.id)"
|
||||
/>
|
||||
{{ player.name }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="player-options">
|
||||
<template v-if="settings.playerId === 'custom'">
|
||||
<div class="player-option-group">
|
||||
<label class="player-option-label">Custom Command</label>
|
||||
<input
|
||||
class="player-option-input"
|
||||
type="text"
|
||||
placeholder='C:\Player\player.exe "%s"'
|
||||
v-model="customCmd"
|
||||
@change="setCustomCmd(customCmd)"
|
||||
/>
|
||||
<p class="player-option-hint">Use %s as placeholder for the file path.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="selectedPlayerFamily === 'mpc'">
|
||||
<div class="player-option-group">
|
||||
<label class="player-option-label" for="mpc-port">MPC Web UI Port</label>
|
||||
<input
|
||||
id="mpc-port"
|
||||
class="player-option-input player-option-input--short"
|
||||
type="number"
|
||||
placeholder="13579"
|
||||
:value="settings.playerMpcPort ?? ''"
|
||||
@input="onMpcPortInput"
|
||||
/>
|
||||
<p class="player-option-hint">
|
||||
Port for MPC-BE/HC web interface. Leave empty to disable remote control.
|
||||
</p>
|
||||
<p v-if="settings.playerMpcPort !== null" class="player-family-note">
|
||||
Web remote control enabled
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section-title">Preferred Format</h2>
|
||||
<p class="settings-section-desc">Preferred format when multiple versions are available.</p>
|
||||
@@ -242,7 +302,8 @@ import { ref, watch, computed, onMounted, onUnmounted } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { navAttrs } from "../composables/useKeyboardNavigation"
|
||||
import logoUrl from "../assets/mediahive.webp"
|
||||
import { fetchRoots, replaceRoots, pickFolderAndAddRoot } from "../api"
|
||||
import { fetchRoots, replaceRoots, pickFolderAndAddRoot, fetchPlayers } from "../api"
|
||||
import type { PlayerInfo } from "../api"
|
||||
import HexKeyboard from "./HexKeyboard.vue"
|
||||
import {
|
||||
useSettings,
|
||||
@@ -252,6 +313,14 @@ import {
|
||||
|
||||
const settings = useSettings()
|
||||
|
||||
const detectedPlayers = ref<PlayerInfo[]>([])
|
||||
const customCmd = ref(settings.playerCustomCmd || "")
|
||||
|
||||
const selectedPlayerFamily = computed(() => {
|
||||
const p = detectedPlayers.value.find((p) => p.id === settings.playerId)
|
||||
return p?.family ?? "default"
|
||||
})
|
||||
|
||||
interface RootEntry {
|
||||
root_id: string
|
||||
name: string
|
||||
@@ -309,6 +378,33 @@ function setPreferredHdr(value: HdrPreference) {
|
||||
settings.preferredHdr = value
|
||||
}
|
||||
|
||||
function setPlayer(id: string) {
|
||||
settings.playerId = id
|
||||
}
|
||||
|
||||
function setCustomCmd(cmd: string) {
|
||||
settings.playerCustomCmd = cmd.trim() || null
|
||||
}
|
||||
|
||||
function onMpcPortInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
const value = target.value.trim()
|
||||
if (value === "") {
|
||||
settings.playerMpcPort = null
|
||||
} else {
|
||||
const num = parseInt(value, 10)
|
||||
settings.playerMpcPort = isNaN(num) || num <= 0 ? null : num
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPlayers() {
|
||||
try {
|
||||
detectedPlayers.value = await fetchPlayers()
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch players:", e)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRoots() {
|
||||
try {
|
||||
const data = await fetchRoots()
|
||||
@@ -360,7 +456,10 @@ async function addRoot() {
|
||||
}
|
||||
|
||||
watch(showSettings, (visible) => {
|
||||
if (visible) void refreshRoots()
|
||||
if (visible) {
|
||||
void refreshRoots()
|
||||
void refreshPlayers()
|
||||
}
|
||||
})
|
||||
|
||||
// Check if we're on a detail page
|
||||
@@ -710,4 +809,84 @@ onUnmounted(() => {
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.player-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 9.5em 1fr;
|
||||
gap: 1.25em;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.player-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.player-radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.player-radio {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.player-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.player-option-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.player-option-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.player-option-input {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.player-option-input:focus {
|
||||
border-color: var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.player-option-input--short {
|
||||
width: 120px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.player-option-hint {
|
||||
margin: 2px 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.player-family-note {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: #22c55e;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user