Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 39 additions & 100 deletions plugins/embyfin-stream-cleanup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@

from .config import (
PLUGIN_CONFIG, PLUGIN_FIELDS, build_plugin_fields, PLUGIN_DB_KEY,
REDIS_KEY_RUNNING, REDIS_KEY_HOST, REDIS_KEY_PORT, REDIS_KEY_STOP,
REDIS_KEY_MONITOR, ALL_PLUGIN_REDIS_KEYS,
REDIS_KEY_STOP, REDIS_KEY_MONITOR, ALL_PLUGIN_REDIS_KEYS,
DEFAULT_PORT, DEFAULT_HOST,
)
from .handler import StreamMonitor
from .server import DebugServer, get_current_server
from .autostart import attempt_autostart
from .utils import get_redis_client, read_redis_flag, normalize_host, redis_decode, prune_stale_server_keys
from .utils import get_redis_client, read_redis_flag, normalize_host, prune_stale_server_keys

logger = logging.getLogger(__name__)

Expand All @@ -35,14 +34,14 @@ class Plugin:

@property
def fields(self):
"""Build fields dynamically based on saved media_server_count."""
"""Regenerate fields from current DB settings on every request."""
try:
from apps.plugins.models import PluginConfig
cfg = PluginConfig.objects.get(key=PLUGIN_DB_KEY)
count = int(cfg.settings.get("media_server_count", 1))
settings = cfg.settings
except Exception:
count = 1
return build_plugin_fields(count)
settings = {}
return build_plugin_fields(settings)

actions = [
{
Expand All @@ -53,14 +52,6 @@ def fields(self):
"button_variant": "filled",
"button_color": "orange",
},
{
"id": "status",
"label": "Status",
"description": "Check monitor and debug server status",
"button_label": "Check Status",
"button_variant": "filled",
"button_color": "blue",
},
{
"id": "reset_settings",
"label": "Reset Settings",
Expand All @@ -79,20 +70,16 @@ def __init__(self):
# -- Action dispatcher -----------------------------------------------------

def _stop_debug_server(self):
"""Stop the debug server if running (local or remote worker)."""
"""Stop the debug server if running in this process.

Local-only, matching force-fallback's pattern: if a different worker
is running the server, this doesn't reach it (no cross-worker Redis
signaling) — that worker keeps its own server until it happens to
handle a restart/stop action itself.
"""
server = get_current_server()
if server and server.is_running():
server.stop()
return

redis_client = get_redis_client()
if redis_client and read_redis_flag(redis_client, REDIS_KEY_RUNNING):
redis_client.set(REDIS_KEY_STOP, "1")
for _ in range(50):
if not read_redis_flag(redis_client, REDIS_KEY_RUNNING):
return
time.sleep(0.1)
redis_client.delete(REDIS_KEY_RUNNING, REDIS_KEY_HOST, REDIS_KEY_PORT, REDIS_KEY_STOP)

def _prune_media_server_settings(self, settings):
"""Remove media server URL/key settings for servers beyond the current count."""
Expand All @@ -117,15 +104,9 @@ def run(self, action: str, params: dict, context: dict):
# Clean up stale media server keys when count decreases
self._prune_media_server_settings(settings)

# Check if debug server was running before we stop it
debug_was_running = False
# Check if the dashboard server was running (in this process) before we stop it
server = get_current_server()
if server and server.is_running():
debug_was_running = True
else:
redis_client = get_redis_client()
if redis_client and read_redis_flag(redis_client, REDIS_KEY_RUNNING):
debug_was_running = True
dash_was_running = bool(server and server.is_running())

self._stop_debug_server()

Expand All @@ -138,63 +119,24 @@ def run(self, action: str, params: dict, context: dict):

msg = "Stream monitor restarted with current settings"

# Start debug server if enabled
if settings.get("enable_debug_server", False):
port = int(settings.get("port", DEFAULT_PORT))
host = normalize_host(settings.get("host", DEFAULT_HOST), DEFAULT_HOST)
# Start dashboard server if enabled
if settings.get("dash_enabled") == "enabled":
port = int(settings.get("dash_port", DEFAULT_PORT))
host = normalize_host(settings.get("dash_host", DEFAULT_HOST), DEFAULT_HOST)
path = settings.get("dash_path") or "/dash"
server = DebugServer(_monitor, port=port, host=host)
if server.start(settings=settings):
msg += f" | Debug server on http://{host}:{port}/debug"
if server.start():
msg += f" | Dashboard on http://{host}:{port}{path}/"
else:
msg += " | Debug server failed to start (port may be in use)"
elif debug_was_running:
msg += " | Debug server stopped (disabled in settings)"
msg += " | Dashboard failed to start (port may be in use)"
elif dash_was_running:
msg += " | Dashboard stopped (disabled in settings)"

return {"status": "success", "message": msg}
except Exception as e:
logger_ctx.error(f"Error restarting monitor: {e}", exc_info=True)
return {"status": "error", "message": f"Failed to restart monitor: {str(e)}"}

# -- status ------------------------------------------------------------
elif action == "status":
monitor_running = _monitor.is_running()
server = get_current_server()
server_running = server and server.is_running()

redis_client = get_redis_client()
remote_monitor = False
remote_server = False
if redis_client:
try:
remote_monitor = read_redis_flag(redis_client, REDIS_KEY_MONITOR)
except Exception:
pass
try:
remote_server = read_redis_flag(redis_client, REDIS_KEY_RUNNING)
except Exception:
pass

parts = []
if monitor_running or remote_monitor:
parts.append("Monitor: running")
else:
parts.append("Monitor: stopped")

if server_running:
parts.append(f"Debug server: http://{server.host}:{server.port}/debug")
elif remote_server:
rhost = redis_decode(redis_client.get(REDIS_KEY_HOST)) or DEFAULT_HOST
rport = redis_decode(redis_client.get(REDIS_KEY_PORT)) or str(DEFAULT_PORT)
parts.append(f"Debug server: http://{rhost}:{rport}/debug (another worker)")
else:
parts.append("Debug server: stopped")

return {
"status": "success",
"message": " | ".join(parts),
"running": monitor_running or remote_monitor,
}

# -- reset_settings ----------------------------------------------------
elif action == "reset_settings":
try:
Expand Down Expand Up @@ -248,11 +190,14 @@ def _reset_all_settings(self):
def stop(self, context: dict):
"""Called when the plugin is disabled or Dispatcharr is shutting down.

After a ``force_reload`` the module-level ``_monitor`` and
``get_current_server()`` references point to *new* (idle) instances
because the module was re-imported. The *old* running daemon threads
are still alive but unreachable by direct reference. We fall back to
Redis signaling so the old poll loops detect the stop flag and exit.
After a ``force_reload`` the module-level ``_monitor`` reference
points to a *new* (idle) instance because the module was re-imported.
The *old* running monitor thread is still alive but unreachable by
direct reference. We fall back to Redis signaling so it detects the
stop flag and exits. The dashboard server has no such fallback (see
``server.py``/``_stop_debug_server``) — an orphaned dashboard server
thread from a previous load only stops if the worker that owns it
happens to handle a subsequent restart/stop action itself.
"""
stopped_monitor = False
if _monitor.is_running():
Expand All @@ -261,22 +206,16 @@ def stop(self, context: dict):
stopped_monitor = True

server = get_current_server()
stopped_server = False
if server and server.is_running():
logger.info("Plugin stopping, shutting down debug server")
logger.info("Plugin stopping, shutting down dashboard server")
server.stop()
stopped_server = True

# Redis fallback: signal orphaned threads from a previous module load
if not stopped_monitor or not stopped_server:
# Redis fallback: signal an orphaned monitor thread from a previous module load
if not stopped_monitor:
redis_client = get_redis_client()
if redis_client:
if not stopped_monitor and read_redis_flag(redis_client, REDIS_KEY_MONITOR):
logger.info("Plugin stopping, sending Redis stop signal to orphaned monitor")
redis_client.set(REDIS_KEY_STOP, "1")
if not stopped_server and read_redis_flag(redis_client, REDIS_KEY_RUNNING):
logger.info("Plugin stopping, sending Redis stop signal to orphaned debug server")
redis_client.set(REDIS_KEY_STOP, "1")
if redis_client and read_redis_flag(redis_client, REDIS_KEY_MONITOR):
logger.info("Plugin stopping, sending Redis stop signal to orphaned monitor")
redis_client.set(REDIS_KEY_STOP, "1")

# Clear leader election and dedup keys so the next discovery can re-autostart
try:
Expand Down
Loading
Loading