Compare commits
6 Commits
8077181dce
...
29e0618b9f
| Author | SHA1 | Date | |
|---|---|---|---|
| 29e0618b9f | |||
| 4f8f59dc89 | |||
| 40c2c11c85 | |||
| 0470a17a0c | |||
| 4635caca98 | |||
| 957a177b72 |
49
CLAUDE.md
49
CLAUDE.md
@@ -26,6 +26,55 @@ To remove the scheduled task:
|
|||||||
Unregister-ScheduledTask -TaskName "MediaServer" -Confirm:$false
|
Unregister-ScheduledTask -TaskName "MediaServer" -Confirm:$false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Server Restart After Code Changes
|
||||||
|
|
||||||
|
**CRITICAL:** When making changes to backend code (Python files, API routes, service logic), the media server MUST be restarted for changes to take effect.
|
||||||
|
|
||||||
|
**When to restart:**
|
||||||
|
|
||||||
|
- Changes to any Python files (`*.py`) in the media_server directory
|
||||||
|
- Changes to API endpoints, routes, or request/response models
|
||||||
|
- Changes to service logic, callbacks, or script execution
|
||||||
|
- Changes to configuration handling or startup logic
|
||||||
|
|
||||||
|
**When restart is NOT needed:**
|
||||||
|
|
||||||
|
- Static file changes (`*.html`, `*.css`, `*.js`, `*.json`) - browser refresh is enough
|
||||||
|
- README or documentation updates
|
||||||
|
- Changes to install/service scripts (only affects new installations)
|
||||||
|
|
||||||
|
**How to restart during development:**
|
||||||
|
|
||||||
|
1. Find the running server process:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr :8765
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
lsof -i :8765
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stop the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
taskkill //F //PID <process_id>
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
kill <process_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the server again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m media_server.main
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practice:** Always restart the server immediately after committing backend changes to verify they work correctly before pushing.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Copy `config.example.yaml` to `config.yaml` and customize.
|
Copy `config.example.yaml` to `config.yaml` and customize.
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -29,6 +29,7 @@ The media server includes a built-in web interface for controlling and monitorin
|
|||||||
- **Token authentication** - Saved in browser localStorage
|
- **Token authentication** - Saved in browser localStorage
|
||||||
- **Responsive design** - Works on desktop and mobile
|
- **Responsive design** - Works on desktop and mobile
|
||||||
- **Dark theme** - Easy on the eyes
|
- **Dark theme** - Easy on the eyes
|
||||||
|
- **Multi-language support** - English and Russian locales with automatic detection
|
||||||
|
|
||||||
### Accessing the Web UI
|
### Accessing the Web UI
|
||||||
|
|
||||||
@@ -56,6 +57,38 @@ To access the Web UI from other devices on your network:
|
|||||||
|
|
||||||
**Security Note:** For remote access over the internet, use a reverse proxy with HTTPS (nginx, Caddy) to encrypt traffic.
|
**Security Note:** For remote access over the internet, use a reverse proxy with HTTPS (nginx, Caddy) to encrypt traffic.
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
The Web UI supports multiple languages with automatic browser locale detection:
|
||||||
|
|
||||||
|
**Available Languages:**
|
||||||
|
|
||||||
|
- **English (en)** - Default
|
||||||
|
- **Русский (ru)** - Russian
|
||||||
|
|
||||||
|
The interface automatically detects your browser language on first visit. You can manually switch languages using the dropdown in the top-right corner of the Web UI.
|
||||||
|
|
||||||
|
**Contributing New Locales:**
|
||||||
|
|
||||||
|
We welcome translations for additional languages! To contribute a new locale:
|
||||||
|
|
||||||
|
1. Copy `media_server/static/locales/en.json` to a new file named with your language code (e.g., `de.json` for German)
|
||||||
|
2. Translate all strings to your language, keeping the same JSON structure
|
||||||
|
3. Add your language to the `supportedLocales` object in `media_server/static/index.html`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const supportedLocales = {
|
||||||
|
'en': 'English',
|
||||||
|
'ru': 'Русский',
|
||||||
|
'de': 'Deutsch' // Add your language here
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Test the translation by switching to your language in the Web UI
|
||||||
|
5. Submit a pull request with your changes
|
||||||
|
|
||||||
|
See [CLAUDE.md](CLAUDE.md#internationalization-i18n) for detailed translation guidelines.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"""Callback management API endpoints."""
|
"""Callback management API endpoints."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
@@ -34,6 +37,18 @@ class CallbackCreateRequest(BaseModel):
|
|||||||
shell: bool = Field(default=True, description="Run command in shell")
|
shell: bool = Field(default=True, description="Run command in shell")
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackExecuteResponse(BaseModel):
|
||||||
|
"""Response model for callback execution."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
callback: str
|
||||||
|
exit_code: int | None = None
|
||||||
|
stdout: str = ""
|
||||||
|
stderr: str = ""
|
||||||
|
error: str | None = None
|
||||||
|
execution_time: float | None = None
|
||||||
|
|
||||||
|
|
||||||
def _validate_callback_name(name: str) -> None:
|
def _validate_callback_name(name: str) -> None:
|
||||||
"""Validate callback name.
|
"""Validate callback name.
|
||||||
|
|
||||||
@@ -84,6 +99,116 @@ async def list_callbacks(_: str = Depends(verify_token)) -> list[CallbackInfo]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/execute/{callback_name}")
|
||||||
|
async def execute_callback(
|
||||||
|
callback_name: str,
|
||||||
|
_: str = Depends(verify_token),
|
||||||
|
) -> CallbackExecuteResponse:
|
||||||
|
"""Execute a callback for debugging purposes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback_name: Name of the callback to execute
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Execution result including stdout, stderr, and exit code
|
||||||
|
"""
|
||||||
|
# Validate callback name
|
||||||
|
_validate_callback_name(callback_name)
|
||||||
|
|
||||||
|
# Check if callback exists
|
||||||
|
if callback_name not in settings.callbacks:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Callback '{callback_name}' not found. Use /api/callbacks/list to see configured callbacks.",
|
||||||
|
)
|
||||||
|
|
||||||
|
callback_config = settings.callbacks[callback_name]
|
||||||
|
|
||||||
|
logger.info(f"Executing callback for debugging: {callback_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute in thread pool to not block
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
result = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: _run_callback(
|
||||||
|
command=callback_config.command,
|
||||||
|
timeout=callback_config.timeout,
|
||||||
|
shell=callback_config.shell,
|
||||||
|
working_dir=callback_config.working_dir,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return CallbackExecuteResponse(
|
||||||
|
success=result["exit_code"] == 0,
|
||||||
|
callback=callback_name,
|
||||||
|
exit_code=result["exit_code"],
|
||||||
|
stdout=result["stdout"],
|
||||||
|
stderr=result["stderr"],
|
||||||
|
execution_time=result.get("execution_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Callback execution error: {e}")
|
||||||
|
return CallbackExecuteResponse(
|
||||||
|
success=False,
|
||||||
|
callback=callback_name,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_callback(
|
||||||
|
command: str,
|
||||||
|
timeout: int,
|
||||||
|
shell: bool,
|
||||||
|
working_dir: str | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Run a callback synchronously.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Command to execute
|
||||||
|
timeout: Timeout in seconds
|
||||||
|
shell: Whether to run in shell
|
||||||
|
working_dir: Working directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with exit_code, stdout, stderr, execution_time
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
shell=shell,
|
||||||
|
cwd=working_dir,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return {
|
||||||
|
"exit_code": result.returncode,
|
||||||
|
"stdout": result.stdout[:10000], # Limit output size
|
||||||
|
"stderr": result.stderr[:10000],
|
||||||
|
"execution_time": execution_time,
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return {
|
||||||
|
"exit_code": -1,
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": f"Callback timed out after {timeout} seconds",
|
||||||
|
"execution_time": execution_time,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return {
|
||||||
|
"exit_code": -1,
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": str(e),
|
||||||
|
"execution_time": execution_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create/{callback_name}")
|
@router.post("/create/{callback_name}")
|
||||||
async def create_callback(
|
async def create_callback(
|
||||||
callback_name: str,
|
callback_name: str,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
@@ -33,6 +34,7 @@ class ScriptExecuteResponse(BaseModel):
|
|||||||
stdout: str = ""
|
stdout: str = ""
|
||||||
stderr: str = ""
|
stderr: str = ""
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
|
execution_time: float | None = None
|
||||||
|
|
||||||
|
|
||||||
class ScriptInfo(BaseModel):
|
class ScriptInfo(BaseModel):
|
||||||
@@ -117,6 +119,7 @@ async def execute_script(
|
|||||||
exit_code=result["exit_code"],
|
exit_code=result["exit_code"],
|
||||||
stdout=result["stdout"],
|
stdout=result["stdout"],
|
||||||
stderr=result["stderr"],
|
stderr=result["stderr"],
|
||||||
|
execution_time=result.get("execution_time"),
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -143,8 +146,9 @@ def _run_script(
|
|||||||
working_dir: Working directory
|
working_dir: Working directory
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with exit_code, stdout, stderr
|
Dict with exit_code, stdout, stderr, execution_time
|
||||||
"""
|
"""
|
||||||
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
command,
|
command,
|
||||||
@@ -154,22 +158,28 @@ def _run_script(
|
|||||||
text=True,
|
text=True,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
return {
|
return {
|
||||||
"exit_code": result.returncode,
|
"exit_code": result.returncode,
|
||||||
"stdout": result.stdout[:10000], # Limit output size
|
"stdout": result.stdout[:10000], # Limit output size
|
||||||
"stderr": result.stderr[:10000],
|
"stderr": result.stderr[:10000],
|
||||||
|
"execution_time": execution_time,
|
||||||
}
|
}
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
return {
|
return {
|
||||||
"exit_code": -1,
|
"exit_code": -1,
|
||||||
"stdout": "",
|
"stdout": "",
|
||||||
"stderr": f"Script timed out after {timeout} seconds",
|
"stderr": f"Script timed out after {timeout} seconds",
|
||||||
|
"execution_time": execution_time,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
return {
|
return {
|
||||||
"exit_code": -1,
|
"exit_code": -1,
|
||||||
"stdout": "",
|
"stdout": "",
|
||||||
"stderr": str(e),
|
"stderr": str(e),
|
||||||
|
"execution_time": execution_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Media Server</title>
|
<title>Media Server</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%231db954;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%231ed760;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='45' fill='url(%23grad)'/%3E%3Cpath fill='white' d='M35 25 L35 75 L75 50 Z'/%3E%3C/svg%3E">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #121212;
|
--bg-primary: #121212;
|
||||||
@@ -44,6 +45,21 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Prevent flash of untranslated content */
|
||||||
|
body.loading-translations {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.1s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.translations-loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent scrolling when dialog is open */
|
||||||
|
body.dialog-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -108,22 +124,30 @@
|
|||||||
fill: var(--text-primary);
|
fill: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#locale-toggle {
|
#locale-select {
|
||||||
background: none;
|
background: var(--bg-tertiary);
|
||||||
border: 2px solid var(--text-secondary);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#locale-toggle:hover {
|
#locale-select:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent);
|
}
|
||||||
transform: scale(1.05);
|
|
||||||
|
#locale-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#locale-select option {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-container {
|
.player-container {
|
||||||
@@ -457,20 +481,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.5rem;
|
||||||
margin-right: 0.5rem;
|
border-radius: 6px;
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.75rem;
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:hover {
|
.action-btn:hover {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.delete:hover {
|
.action-btn.delete:hover {
|
||||||
@@ -478,6 +512,95 @@
|
|||||||
border-color: var(--error);
|
border-color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.execute:hover {
|
||||||
|
background: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execution Result Dialog */
|
||||||
|
.execution-result {
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-result pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-size: 0.813rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-status {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.success value {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.error value {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid var(--bg-tertiary);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Dialog Styles */
|
/* Dialog Styles */
|
||||||
dialog {
|
dialog {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
@@ -486,9 +609,15 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
margin: auto;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure dialogs are hidden until explicitly opened */
|
||||||
|
dialog:not([open]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
@@ -550,13 +679,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer button {
|
.dialog-footer button {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.625rem 1.5rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
min-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer .btn-primary {
|
.dialog-footer .btn-primary {
|
||||||
@@ -757,14 +888,40 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a:hover {
|
||||||
|
color: var(--accent-hover);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer .separator {
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="loading-translations">
|
||||||
<!-- Clear Token Button -->
|
<!-- Clear Token Button -->
|
||||||
<button class="clear-token-btn" onclick="clearToken()" data-i18n-title="auth.logout.title" data-i18n="auth.logout" title="Clear saved token">Logout</button>
|
<button class="clear-token-btn" onclick="clearToken()" data-i18n-title="auth.logout.title" data-i18n="auth.logout" title="Clear saved token">Logout</button>
|
||||||
|
|
||||||
<!-- Auth Modal -->
|
<!-- Auth Modal -->
|
||||||
<div id="auth-overlay">
|
<div id="auth-overlay" class="hidden">
|
||||||
<div class="auth-modal">
|
<div class="auth-modal">
|
||||||
<h2 data-i18n="app.title">Media Server</h2>
|
<h2 data-i18n="app.title">Media Server</h2>
|
||||||
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
|
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
|
||||||
@@ -790,7 +947,10 @@
|
|||||||
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
|
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="locale-toggle" onclick="toggleLocale()" data-i18n-title="player.locale" title="Change language">EN</button>
|
<select id="locale-select" onchange="changeLocale()" title="Change language">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="ru">Русский</option>
|
||||||
|
</select>
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
<span class="status-dot" id="status-dot"></span>
|
<span class="status-dot" id="status-dot"></span>
|
||||||
<span id="status-text" data-i18n="player.status.disconnected">Disconnected</span>
|
<span id="status-text" data-i18n="player.status.disconnected">Disconnected</span>
|
||||||
@@ -920,7 +1080,7 @@
|
|||||||
<!-- Add/Edit Script Dialog -->
|
<!-- Add/Edit Script Dialog -->
|
||||||
<dialog id="scriptDialog">
|
<dialog id="scriptDialog">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="dialogTitle">Add Script</h3>
|
<h3 id="dialogTitle" data-i18n="scripts.dialog.add">Add Script</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="scriptForm" onsubmit="saveScript(event)">
|
<form id="scriptForm" onsubmit="saveScript(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
@@ -928,39 +1088,39 @@
|
|||||||
<input type="hidden" id="scriptIsEdit">
|
<input type="hidden" id="scriptIsEdit">
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Script Name *
|
<span data-i18n="scripts.field.name">Script Name *</span>
|
||||||
<input type="text" id="scriptName" required pattern="[a-zA-Z0-9_]+"
|
<input type="text" id="scriptName" required pattern="[a-zA-Z0-9_]+"
|
||||||
title="Only letters, numbers, and underscores allowed" maxlength="64">
|
data-i18n-title="scripts.placeholder.name" title="Only letters, numbers, and underscores allowed" maxlength="64">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Label
|
<span data-i18n="scripts.field.label">Label</span>
|
||||||
<input type="text" id="scriptLabel" placeholder="Human-readable name">
|
<input type="text" id="scriptLabel" data-i18n-placeholder="scripts.placeholder.label" placeholder="Human-readable name">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Command *
|
<span data-i18n="scripts.field.command">Command *</span>
|
||||||
<input type="text" id="scriptCommand" required placeholder="e.g., shutdown /s /t 0">
|
<textarea id="scriptCommand" required rows="3" data-i18n-placeholder="scripts.placeholder.command" placeholder="e.g., shutdown /s /t 0"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Description
|
<span data-i18n="scripts.field.description">Description</span>
|
||||||
<textarea id="scriptDescription" placeholder="What does this script do?"></textarea>
|
<textarea id="scriptDescription" data-i18n-placeholder="scripts.placeholder.description" placeholder="What does this script do?"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Icon (MDI)
|
<span data-i18n="scripts.field.icon">Icon (MDI)</span>
|
||||||
<input type="text" id="scriptIcon" placeholder="e.g., mdi:power">
|
<input type="text" id="scriptIcon" data-i18n-placeholder="scripts.placeholder.icon" placeholder="e.g., mdi:power">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Timeout (seconds)
|
<span data-i18n="scripts.field.timeout">Timeout (seconds)</span>
|
||||||
<input type="number" id="scriptTimeout" value="30" min="1" max="300">
|
<input type="number" id="scriptTimeout" value="30" min="1" max="300">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeScriptDialog()">Cancel</button>
|
<button type="button" class="btn-secondary" onclick="closeScriptDialog()" data-i18n="scripts.button.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary">Save</button>
|
<button type="submit" class="btn-primary" data-i18n="scripts.button.save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -968,55 +1128,91 @@
|
|||||||
<!-- Add/Edit Callback Dialog -->
|
<!-- Add/Edit Callback Dialog -->
|
||||||
<dialog id="callbackDialog">
|
<dialog id="callbackDialog">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="callbackDialogTitle">Add Callback</h3>
|
<h3 id="callbackDialogTitle" data-i18n="callbacks.dialog.add">Add Callback</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="callbackForm" onsubmit="saveCallback(event)">
|
<form id="callbackForm" onsubmit="saveCallback(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<input type="hidden" id="callbackIsEdit">
|
<input type="hidden" id="callbackIsEdit">
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Event *
|
<span data-i18n="callbacks.field.event">Event *</span>
|
||||||
<select id="callbackName" required>
|
<select id="callbackName" required>
|
||||||
<option value="">Select event...</option>
|
<option value="" data-i18n="callbacks.placeholder.event">Select event...</option>
|
||||||
<option value="on_play">on_play - After play succeeds</option>
|
<option value="on_play" data-i18n="callbacks.event.on_play">on_play - After play succeeds</option>
|
||||||
<option value="on_pause">on_pause - After pause succeeds</option>
|
<option value="on_pause" data-i18n="callbacks.event.on_pause">on_pause - After pause succeeds</option>
|
||||||
<option value="on_stop">on_stop - After stop succeeds</option>
|
<option value="on_stop" data-i18n="callbacks.event.on_stop">on_stop - After stop succeeds</option>
|
||||||
<option value="on_next">on_next - After next track succeeds</option>
|
<option value="on_next" data-i18n="callbacks.event.on_next">on_next - After next track succeeds</option>
|
||||||
<option value="on_previous">on_previous - After previous track succeeds</option>
|
<option value="on_previous" data-i18n="callbacks.event.on_previous">on_previous - After previous track succeeds</option>
|
||||||
<option value="on_volume">on_volume - After volume change</option>
|
<option value="on_volume" data-i18n="callbacks.event.on_volume">on_volume - After volume change</option>
|
||||||
<option value="on_mute">on_mute - After mute toggle</option>
|
<option value="on_mute" data-i18n="callbacks.event.on_mute">on_mute - After mute toggle</option>
|
||||||
<option value="on_seek">on_seek - After seek succeeds</option>
|
<option value="on_seek" data-i18n="callbacks.event.on_seek">on_seek - After seek succeeds</option>
|
||||||
<option value="on_turn_on">on_turn_on - Callback-only action</option>
|
<option value="on_turn_on" data-i18n="callbacks.event.on_turn_on">on_turn_on - Callback-only action</option>
|
||||||
<option value="on_turn_off">on_turn_off - Callback-only action</option>
|
<option value="on_turn_off" data-i18n="callbacks.event.on_turn_off">on_turn_off - Callback-only action</option>
|
||||||
<option value="on_toggle">on_toggle - Callback-only action</option>
|
<option value="on_toggle" data-i18n="callbacks.event.on_toggle">on_toggle - Callback-only action</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Command *
|
<span data-i18n="callbacks.field.command">Command *</span>
|
||||||
<input type="text" id="callbackCommand" required placeholder="e.g., shutdown /s /t 0">
|
<textarea id="callbackCommand" required rows="3" data-i18n-placeholder="callbacks.placeholder.command" placeholder="e.g., shutdown /s /t 0"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Timeout (seconds)
|
<span data-i18n="callbacks.field.timeout">Timeout (seconds)</span>
|
||||||
<input type="number" id="callbackTimeout" value="30" min="1" max="300">
|
<input type="number" id="callbackTimeout" value="30" min="1" max="300">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Working Directory
|
<span data-i18n="callbacks.field.workdir">Working Directory</span>
|
||||||
<input type="text" id="callbackWorkingDir" placeholder="Optional">
|
<input type="text" id="callbackWorkingDir" data-i18n-placeholder="callbacks.placeholder.workdir" placeholder="Optional">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeCallbackDialog()">Cancel</button>
|
<button type="button" class="btn-secondary" onclick="closeCallbackDialog()" data-i18n="callbacks.button.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary">Save</button>
|
<button type="submit" class="btn-primary" data-i18n="callbacks.button.save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Execution Result Dialog -->
|
||||||
|
<dialog id="executionDialog">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h3 id="executionDialogTitle">Execution Result</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="execution-status" id="executionStatus"></div>
|
||||||
|
<div class="result-section" id="outputSection" style="display: none;">
|
||||||
|
<h4>Output</h4>
|
||||||
|
<div class="execution-result">
|
||||||
|
<pre id="executionOutput"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-section" id="errorSection" style="display: none;">
|
||||||
|
<h4>Error Output</h4>
|
||||||
|
<div class="execution-result">
|
||||||
|
<pre id="executionError"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button type="button" class="btn-secondary" onclick="closeExecutionDialog()">Close</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Toast Notification -->
|
<!-- Toast Notification -->
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
Created by <strong>Alexei Dolgolyov</strong>
|
||||||
|
<span class="separator">•</span>
|
||||||
|
<a href="mailto:dolgolyov.alexei@gmail.com">dolgolyov.alexei@gmail.com</a>
|
||||||
|
<span class="separator">•</span>
|
||||||
|
<a href="https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Theme management
|
// Theme management
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
@@ -1049,7 +1245,10 @@
|
|||||||
// Locale management
|
// Locale management
|
||||||
let currentLocale = 'en';
|
let currentLocale = 'en';
|
||||||
let translations = {};
|
let translations = {};
|
||||||
const supportedLocales = ['en', 'ru'];
|
const supportedLocales = {
|
||||||
|
'en': 'English',
|
||||||
|
'ru': 'Русский'
|
||||||
|
};
|
||||||
|
|
||||||
// Minimal inline fallback for critical UI elements
|
// Minimal inline fallback for critical UI elements
|
||||||
const fallbackTranslations = {
|
const fallbackTranslations = {
|
||||||
@@ -1096,7 +1295,7 @@
|
|||||||
const langCode = browserLang.split('-')[0]; // 'en-US' -> 'en', 'ru-RU' -> 'ru'
|
const langCode = browserLang.split('-')[0]; // 'en-US' -> 'en', 'ru-RU' -> 'ru'
|
||||||
|
|
||||||
// Only return if we support it
|
// Only return if we support it
|
||||||
return supportedLocales.includes(langCode) ? langCode : 'en';
|
return supportedLocales[langCode] ? langCode : 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize locale
|
// Initialize locale
|
||||||
@@ -1107,7 +1306,7 @@
|
|||||||
|
|
||||||
// Set locale
|
// Set locale
|
||||||
async function setLocale(locale) {
|
async function setLocale(locale) {
|
||||||
if (!supportedLocales.includes(locale)) {
|
if (!supportedLocales[locale]) {
|
||||||
locale = 'en';
|
locale = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,22 +1321,29 @@
|
|||||||
// Update all text
|
// Update all text
|
||||||
updateAllText();
|
updateAllText();
|
||||||
|
|
||||||
// Update locale toggle button (if visible)
|
// Update locale select dropdown (if visible)
|
||||||
updateLocaleToggle();
|
updateLocaleSelect();
|
||||||
|
|
||||||
|
// Remove loading class and show content
|
||||||
|
document.body.classList.remove('loading-translations');
|
||||||
|
document.body.classList.add('translations-loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle between locales
|
// Change locale from dropdown
|
||||||
async function toggleLocale() {
|
function changeLocale() {
|
||||||
const newLocale = currentLocale === 'en' ? 'ru' : 'en';
|
const select = document.getElementById('locale-select');
|
||||||
await setLocale(newLocale);
|
const newLocale = select.value;
|
||||||
|
if (newLocale && newLocale !== currentLocale) {
|
||||||
|
localStorage.setItem('locale', newLocale);
|
||||||
|
setLocale(newLocale);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update locale toggle button
|
// Update locale select dropdown
|
||||||
function updateLocaleToggle() {
|
function updateLocaleSelect() {
|
||||||
const localeButton = document.getElementById('locale-toggle');
|
const select = document.getElementById('locale-select');
|
||||||
if (localeButton) {
|
if (select) {
|
||||||
localeButton.textContent = currentLocale === 'en' ? 'RU' : 'EN';
|
select.value = currentLocale;
|
||||||
localeButton.title = t('player.locale');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,6 +1398,10 @@
|
|||||||
let scripts = [];
|
let scripts = [];
|
||||||
let lastStatus = null; // Store last status for locale switching
|
let lastStatus = null; // Store last status for locale switching
|
||||||
|
|
||||||
|
// Dialog dirty state tracking
|
||||||
|
let scriptFormDirty = false;
|
||||||
|
let callbackFormDirty = false;
|
||||||
|
|
||||||
// Position interpolation
|
// Position interpolation
|
||||||
let lastPositionUpdate = 0;
|
let lastPositionUpdate = 0;
|
||||||
let lastPositionValue = 0;
|
let lastPositionValue = 0;
|
||||||
@@ -1247,6 +1457,42 @@
|
|||||||
authenticate();
|
authenticate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Script form dirty state tracking
|
||||||
|
const scriptForm = document.getElementById('scriptForm');
|
||||||
|
scriptForm.addEventListener('input', () => {
|
||||||
|
scriptFormDirty = true;
|
||||||
|
});
|
||||||
|
scriptForm.addEventListener('change', () => {
|
||||||
|
scriptFormDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback form dirty state tracking
|
||||||
|
const callbackForm = document.getElementById('callbackForm');
|
||||||
|
callbackForm.addEventListener('input', () => {
|
||||||
|
callbackFormDirty = true;
|
||||||
|
});
|
||||||
|
callbackForm.addEventListener('change', () => {
|
||||||
|
callbackFormDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Script dialog backdrop click to close
|
||||||
|
const scriptDialog = document.getElementById('scriptDialog');
|
||||||
|
scriptDialog.addEventListener('click', (e) => {
|
||||||
|
// Check if click is on the backdrop (not the dialog content)
|
||||||
|
if (e.target === scriptDialog) {
|
||||||
|
closeScriptDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback dialog backdrop click to close
|
||||||
|
const callbackDialog = document.getElementById('callbackDialog');
|
||||||
|
callbackDialog.addEventListener('click', (e) => {
|
||||||
|
// Check if click is on the backdrop (not the dialog content)
|
||||||
|
if (e.target === callbackDialog) {
|
||||||
|
closeCallbackDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function showAuthForm(errorMessage = '') {
|
function showAuthForm(errorMessage = '') {
|
||||||
@@ -1676,12 +1922,21 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><code>${script.name}</code></td>
|
<td><code>${script.name}</code></td>
|
||||||
<td>${script.label || script.name}</td>
|
<td>${script.label || script.name}</td>
|
||||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||||
title="${escapeHtml(script.command || 'N/A')}">${escapeHtml(script.command || 'N/A')}</td>
|
title="${escapeHtml(script.command || 'N/A')}">${escapeHtml(script.command || 'N/A')}</td>
|
||||||
<td>${script.timeout}s</td>
|
<td>${script.timeout}s</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="action-btn" onclick="showEditScriptDialog('${script.name}')">Edit</button>
|
<div class="action-buttons">
|
||||||
<button class="action-btn delete" onclick="deleteScriptConfirm('${script.name}')">Delete</button>
|
<button class="action-btn execute" onclick="executeScriptDebug('${script.name}')" title="Execute script">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn" onclick="showEditScriptDialog('${script.name}')" title="Edit script">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn delete" onclick="deleteScriptConfirm('${script.name}')" title="Delete script">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -1707,8 +1962,12 @@
|
|||||||
document.getElementById('scriptOriginalName').value = '';
|
document.getElementById('scriptOriginalName').value = '';
|
||||||
document.getElementById('scriptIsEdit').value = 'false';
|
document.getElementById('scriptIsEdit').value = 'false';
|
||||||
document.getElementById('scriptName').disabled = false;
|
document.getElementById('scriptName').disabled = false;
|
||||||
title.textContent = 'Add Script';
|
title.textContent = t('scripts.dialog.add');
|
||||||
|
|
||||||
|
// Reset dirty state
|
||||||
|
scriptFormDirty = false;
|
||||||
|
|
||||||
|
document.body.classList.add('dialog-open');
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1746,7 +2005,12 @@
|
|||||||
document.getElementById('scriptIcon').value = script.icon || '';
|
document.getElementById('scriptIcon').value = script.icon || '';
|
||||||
document.getElementById('scriptTimeout').value = script.timeout || 30;
|
document.getElementById('scriptTimeout').value = script.timeout || 30;
|
||||||
|
|
||||||
title.textContent = 'Edit Script';
|
title.textContent = t('scripts.dialog.edit');
|
||||||
|
|
||||||
|
// Reset dirty state
|
||||||
|
scriptFormDirty = false;
|
||||||
|
|
||||||
|
document.body.classList.add('dialog-open');
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading script for edit:', error);
|
console.error('Error loading script for edit:', error);
|
||||||
@@ -1755,8 +2019,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeScriptDialog() {
|
function closeScriptDialog() {
|
||||||
|
// Check if form has unsaved changes
|
||||||
|
if (scriptFormDirty) {
|
||||||
|
if (!confirm(t('scripts.confirm.unsaved'))) {
|
||||||
|
return; // User cancelled, don't close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dialog = document.getElementById('scriptDialog');
|
const dialog = document.getElementById('scriptDialog');
|
||||||
|
scriptFormDirty = false; // Reset dirty state
|
||||||
dialog.close();
|
dialog.close();
|
||||||
|
document.body.classList.remove('dialog-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveScript(event) {
|
async function saveScript(event) {
|
||||||
@@ -1797,6 +2070,7 @@
|
|||||||
|
|
||||||
if (response.ok && result.success) {
|
if (response.ok && result.success) {
|
||||||
showToast(`Script ${isEdit ? 'updated' : 'created'} successfully`, 'success');
|
showToast(`Script ${isEdit ? 'updated' : 'created'} successfully`, 'success');
|
||||||
|
scriptFormDirty = false; // Reset dirty state before closing
|
||||||
closeScriptDialog();
|
closeScriptDialog();
|
||||||
// Don't reload manually - WebSocket will trigger it
|
// Don't reload manually - WebSocket will trigger it
|
||||||
} else {
|
} else {
|
||||||
@@ -1862,12 +2136,21 @@
|
|||||||
tbody.innerHTML = callbacksList.map(callback => `
|
tbody.innerHTML = callbacksList.map(callback => `
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>${callback.name}</code></td>
|
<td><code>${callback.name}</code></td>
|
||||||
<td style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||||
title="${escapeHtml(callback.command)}">${escapeHtml(callback.command)}</td>
|
title="${escapeHtml(callback.command)}">${escapeHtml(callback.command)}</td>
|
||||||
<td>${callback.timeout}s</td>
|
<td>${callback.timeout}s</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="action-btn" onclick="showEditCallbackDialog('${callback.name}')">Edit</button>
|
<div class="action-buttons">
|
||||||
<button class="action-btn delete" onclick="deleteCallbackConfirm('${callback.name}')">Delete</button>
|
<button class="action-btn execute" onclick="executeCallbackDebug('${callback.name}')" title="Execute callback">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn" onclick="showEditCallbackDialog('${callback.name}')" title="Edit callback">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn delete" onclick="deleteCallbackConfirm('${callback.name}')" title="Delete callback">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -1886,8 +2169,12 @@
|
|||||||
form.reset();
|
form.reset();
|
||||||
document.getElementById('callbackIsEdit').value = 'false';
|
document.getElementById('callbackIsEdit').value = 'false';
|
||||||
document.getElementById('callbackName').disabled = false;
|
document.getElementById('callbackName').disabled = false;
|
||||||
title.textContent = 'Add Callback';
|
title.textContent = t('callbacks.dialog.add');
|
||||||
|
|
||||||
|
// Reset dirty state
|
||||||
|
callbackFormDirty = false;
|
||||||
|
|
||||||
|
document.body.classList.add('dialog-open');
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1922,7 +2209,12 @@
|
|||||||
document.getElementById('callbackTimeout').value = callback.timeout;
|
document.getElementById('callbackTimeout').value = callback.timeout;
|
||||||
document.getElementById('callbackWorkingDir').value = callback.working_dir || '';
|
document.getElementById('callbackWorkingDir').value = callback.working_dir || '';
|
||||||
|
|
||||||
title.textContent = 'Edit Callback';
|
title.textContent = t('callbacks.dialog.edit');
|
||||||
|
|
||||||
|
// Reset dirty state
|
||||||
|
callbackFormDirty = false;
|
||||||
|
|
||||||
|
document.body.classList.add('dialog-open');
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading callback for edit:', error);
|
console.error('Error loading callback for edit:', error);
|
||||||
@@ -1931,8 +2223,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeCallbackDialog() {
|
function closeCallbackDialog() {
|
||||||
|
// Check if form has unsaved changes
|
||||||
|
if (callbackFormDirty) {
|
||||||
|
if (!confirm(t('callbacks.confirm.unsaved'))) {
|
||||||
|
return; // User cancelled, don't close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dialog = document.getElementById('callbackDialog');
|
const dialog = document.getElementById('callbackDialog');
|
||||||
|
callbackFormDirty = false; // Reset dirty state
|
||||||
dialog.close();
|
dialog.close();
|
||||||
|
document.body.classList.remove('dialog-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCallback(event) {
|
async function saveCallback(event) {
|
||||||
@@ -1969,6 +2270,7 @@
|
|||||||
|
|
||||||
if (response.ok && result.success) {
|
if (response.ok && result.success) {
|
||||||
showToast(`Callback ${isEdit ? 'updated' : 'created'} successfully`, 'success');
|
showToast(`Callback ${isEdit ? 'updated' : 'created'} successfully`, 'success');
|
||||||
|
callbackFormDirty = false; // Reset dirty state before closing
|
||||||
closeCallbackDialog();
|
closeCallbackDialog();
|
||||||
loadCallbacksTable();
|
loadCallbacksTable();
|
||||||
} else {
|
} else {
|
||||||
@@ -2008,6 +2310,179 @@
|
|||||||
showToast('Error deleting callback', 'error');
|
showToast('Error deleting callback', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execution Result Dialog Functions
|
||||||
|
|
||||||
|
function closeExecutionDialog() {
|
||||||
|
const dialog = document.getElementById('executionDialog');
|
||||||
|
dialog.close();
|
||||||
|
document.body.classList.remove('dialog-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showExecutionResult(name, result, type = 'script') {
|
||||||
|
const dialog = document.getElementById('executionDialog');
|
||||||
|
const title = document.getElementById('executionDialogTitle');
|
||||||
|
const statusDiv = document.getElementById('executionStatus');
|
||||||
|
const outputSection = document.getElementById('outputSection');
|
||||||
|
const errorSection = document.getElementById('errorSection');
|
||||||
|
const outputPre = document.getElementById('executionOutput');
|
||||||
|
const errorPre = document.getElementById('executionError');
|
||||||
|
|
||||||
|
// Set title
|
||||||
|
title.textContent = `Execution Result: ${name}`;
|
||||||
|
|
||||||
|
// Build status display
|
||||||
|
const success = result.success && result.exit_code === 0;
|
||||||
|
const statusClass = success ? 'success' : 'error';
|
||||||
|
const statusText = success ? 'Success' : 'Failed';
|
||||||
|
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status-item ${statusClass}">
|
||||||
|
<label>Status</label>
|
||||||
|
<value>${statusText}</value>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<label>Exit Code</label>
|
||||||
|
<value>${result.exit_code !== undefined ? result.exit_code : 'N/A'}</value>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<label>Duration</label>
|
||||||
|
<value>${result.execution_time !== undefined && result.execution_time !== null ? result.execution_time.toFixed(3) + 's' : 'N/A'}</value>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Always show output section
|
||||||
|
outputSection.style.display = 'block';
|
||||||
|
if (result.stdout && result.stdout.trim()) {
|
||||||
|
outputPre.textContent = result.stdout;
|
||||||
|
} else {
|
||||||
|
outputPre.textContent = '(no output)';
|
||||||
|
outputPre.style.fontStyle = 'italic';
|
||||||
|
outputPre.style.color = 'var(--text-secondary)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error output if present
|
||||||
|
if (result.stderr && result.stderr.trim()) {
|
||||||
|
errorSection.style.display = 'block';
|
||||||
|
errorPre.textContent = result.stderr;
|
||||||
|
errorPre.style.fontStyle = 'normal';
|
||||||
|
errorPre.style.color = 'var(--error)';
|
||||||
|
} else if (!success && result.error) {
|
||||||
|
errorSection.style.display = 'block';
|
||||||
|
errorPre.textContent = result.error;
|
||||||
|
errorPre.style.fontStyle = 'normal';
|
||||||
|
errorPre.style.color = 'var(--error)';
|
||||||
|
} else {
|
||||||
|
errorSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeScriptDebug(scriptName) {
|
||||||
|
const token = localStorage.getItem('media_server_token');
|
||||||
|
const dialog = document.getElementById('executionDialog');
|
||||||
|
const title = document.getElementById('executionDialogTitle');
|
||||||
|
const statusDiv = document.getElementById('executionStatus');
|
||||||
|
|
||||||
|
// Show dialog with loading state
|
||||||
|
title.textContent = `Executing: ${scriptName}`;
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status-item">
|
||||||
|
<label>Status</label>
|
||||||
|
<value><span class="loading-spinner"></span> Running...</value>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('outputSection').style.display = 'none';
|
||||||
|
document.getElementById('errorSection').style.display = 'none';
|
||||||
|
document.body.classList.add('dialog-open');
|
||||||
|
dialog.showModal();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/scripts/execute/${scriptName}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ args: [] })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showExecutionResult(scriptName, result, 'script');
|
||||||
|
} else {
|
||||||
|
showExecutionResult(scriptName, {
|
||||||
|
success: false,
|
||||||
|
exit_code: -1,
|
||||||
|
error: result.detail || 'Execution failed',
|
||||||
|
stderr: result.detail || 'Unknown error'
|
||||||
|
}, 'script');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error executing script ${scriptName}:`, error);
|
||||||
|
showExecutionResult(scriptName, {
|
||||||
|
success: false,
|
||||||
|
exit_code: -1,
|
||||||
|
error: error.message,
|
||||||
|
stderr: `Network error: ${error.message}`
|
||||||
|
}, 'script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCallbackDebug(callbackName) {
|
||||||
|
const token = localStorage.getItem('media_server_token');
|
||||||
|
const dialog = document.getElementById('executionDialog');
|
||||||
|
const title = document.getElementById('executionDialogTitle');
|
||||||
|
const statusDiv = document.getElementById('executionStatus');
|
||||||
|
|
||||||
|
// Show dialog with loading state
|
||||||
|
title.textContent = `Executing: ${callbackName}`;
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status-item">
|
||||||
|
<label>Status</label>
|
||||||
|
<value><span class="loading-spinner"></span> Running...</value>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('outputSection').style.display = 'none';
|
||||||
|
document.getElementById('errorSection').style.display = 'none';
|
||||||
|
document.body.classList.add('dialog-open');
|
||||||
|
dialog.showModal();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For callbacks, we'll execute them directly via the callback endpoint
|
||||||
|
// We need to trigger the callback as if the event occurred
|
||||||
|
const response = await fetch(`/api/callbacks/execute/${callbackName}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showExecutionResult(callbackName, result, 'callback');
|
||||||
|
} else {
|
||||||
|
showExecutionResult(callbackName, {
|
||||||
|
success: false,
|
||||||
|
exit_code: -1,
|
||||||
|
error: result.detail || 'Execution failed',
|
||||||
|
stderr: result.detail || 'Unknown error'
|
||||||
|
}, 'callback');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error executing callback ${callbackName}:`, error);
|
||||||
|
showExecutionResult(callbackName, {
|
||||||
|
success: false,
|
||||||
|
exit_code: -1,
|
||||||
|
error: error.message,
|
||||||
|
stderr: `Network error: ${error.message}`
|
||||||
|
}, 'callback');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"scripts.msg.load_failed": "Failed to load script details",
|
"scripts.msg.load_failed": "Failed to load script details",
|
||||||
"scripts.msg.list_failed": "Failed to load scripts",
|
"scripts.msg.list_failed": "Failed to load scripts",
|
||||||
"scripts.confirm.delete": "Are you sure you want to delete the script \"{name}\"?",
|
"scripts.confirm.delete": "Are you sure you want to delete the script \"{name}\"?",
|
||||||
|
"scripts.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?",
|
||||||
"callbacks.management": "Callback Management",
|
"callbacks.management": "Callback Management",
|
||||||
"callbacks.description": "Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)",
|
"callbacks.description": "Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)",
|
||||||
"callbacks.add": "Add",
|
"callbacks.add": "Add",
|
||||||
@@ -105,5 +106,6 @@
|
|||||||
"callbacks.msg.not_found": "Callback not found",
|
"callbacks.msg.not_found": "Callback not found",
|
||||||
"callbacks.msg.load_failed": "Failed to load callback details",
|
"callbacks.msg.load_failed": "Failed to load callback details",
|
||||||
"callbacks.msg.list_failed": "Failed to load callbacks",
|
"callbacks.msg.list_failed": "Failed to load callbacks",
|
||||||
"callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?"
|
"callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?",
|
||||||
|
"callbacks.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"scripts.msg.load_failed": "Не удалось загрузить данные скрипта",
|
"scripts.msg.load_failed": "Не удалось загрузить данные скрипта",
|
||||||
"scripts.msg.list_failed": "Не удалось загрузить скрипты",
|
"scripts.msg.list_failed": "Не удалось загрузить скрипты",
|
||||||
"scripts.confirm.delete": "Вы уверены, что хотите удалить скрипт \"{name}\"?",
|
"scripts.confirm.delete": "Вы уверены, что хотите удалить скрипт \"{name}\"?",
|
||||||
|
"scripts.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
||||||
"callbacks.management": "Управление Обратными Вызовами",
|
"callbacks.management": "Управление Обратными Вызовами",
|
||||||
"callbacks.description": "Обратные вызовы - это скрипты, автоматически запускаемые при событиях управления медиа (воспроизведение, пауза, остановка и т.д.)",
|
"callbacks.description": "Обратные вызовы - это скрипты, автоматически запускаемые при событиях управления медиа (воспроизведение, пауза, остановка и т.д.)",
|
||||||
"callbacks.add": "Добавить",
|
"callbacks.add": "Добавить",
|
||||||
@@ -105,5 +106,6 @@
|
|||||||
"callbacks.msg.not_found": "Обратный вызов не найден",
|
"callbacks.msg.not_found": "Обратный вызов не найден",
|
||||||
"callbacks.msg.load_failed": "Не удалось загрузить данные обратного вызова",
|
"callbacks.msg.load_failed": "Не удалось загрузить данные обратного вызова",
|
||||||
"callbacks.msg.list_failed": "Не удалось загрузить обратные вызовы",
|
"callbacks.msg.list_failed": "Не удалось загрузить обратные вызовы",
|
||||||
"callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?"
|
"callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?",
|
||||||
|
"callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?"
|
||||||
}
|
}
|
||||||
|
|||||||
24
scripts/restart-server.bat
Normal file
24
scripts/restart-server.bat
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@echo off
|
||||||
|
REM Media Server Restart Script
|
||||||
|
REM This script restarts the media server
|
||||||
|
|
||||||
|
echo Restarting Media Server...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Stop the server first
|
||||||
|
echo [1/2] Stopping server...
|
||||||
|
call "%~dp0\stop-server.bat"
|
||||||
|
|
||||||
|
REM Wait a moment
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
|
||||||
|
REM Change to parent directory (media-server root)
|
||||||
|
cd /d "%~dp0\.."
|
||||||
|
|
||||||
|
REM Start the server
|
||||||
|
echo.
|
||||||
|
echo [2/2] Starting server...
|
||||||
|
python -m media_server.main
|
||||||
|
|
||||||
|
REM If the server exits, pause to show any error messages
|
||||||
|
pause
|
||||||
7
scripts/start-server-background.vbs
Normal file
7
scripts/start-server-background.vbs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Set WshShell = CreateObject("WScript.Shell")
|
||||||
|
Set FSO = CreateObject("Scripting.FileSystemObject")
|
||||||
|
' Get parent folder of scripts folder (media-server root)
|
||||||
|
WshShell.CurrentDirectory = FSO.GetParentFolderName(FSO.GetParentFolderName(WScript.ScriptFullName))
|
||||||
|
WshShell.Run "python -m media_server.main", 0, False
|
||||||
|
Set FSO = Nothing
|
||||||
|
Set WshShell = Nothing
|
||||||
15
scripts/start-server.bat
Normal file
15
scripts/start-server.bat
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@echo off
|
||||||
|
REM Media Server Startup Script
|
||||||
|
REM This script starts the media server
|
||||||
|
|
||||||
|
echo Starting Media Server...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Change to the media-server directory (parent of scripts folder)
|
||||||
|
cd /d "%~dp0\.."
|
||||||
|
|
||||||
|
REM Start the media server
|
||||||
|
python -m media_server.main
|
||||||
|
|
||||||
|
REM If the server exits, pause to show any error messages
|
||||||
|
pause
|
||||||
19
scripts/stop-server.bat
Normal file
19
scripts/stop-server.bat
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@echo off
|
||||||
|
REM Media Server Stop Script
|
||||||
|
REM This script stops the running media server
|
||||||
|
|
||||||
|
echo Stopping Media Server...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Find and kill Python processes running media_server.main
|
||||||
|
for /f "tokens=2" %%i in ('tasklist /FI "IMAGENAME eq python.exe" /FO LIST ^| findstr /B "PID:"') do (
|
||||||
|
wmic process where "ProcessId=%%i" get CommandLine 2>nul | findstr /C:"media_server.main" >nul
|
||||||
|
if not errorlevel 1 (
|
||||||
|
taskkill /PID %%i /F
|
||||||
|
echo Media server process (PID %%i) terminated.
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Done! Media server stopped.
|
||||||
|
pause
|
||||||
Reference in New Issue
Block a user