dfd
This commit is contained in:
125
cmdnats/pipe.py
125
cmdnats/pipe.py
@@ -5,7 +5,7 @@ import platform
|
||||
import socket
|
||||
import re
|
||||
import subprocess
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from pathlib import Path
|
||||
from cmdlets._shared import Cmdlet, CmdletArg, parse_cmdlet_args
|
||||
from helper.logger import log, debug
|
||||
@@ -87,6 +87,37 @@ def _extract_target_from_memory_uri(text: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_playlist_target(text: Optional[str]) -> Optional[str]:
|
||||
"""Normalize playlist entry targets for dedupe comparisons."""
|
||||
if not text:
|
||||
return None
|
||||
real = _extract_target_from_memory_uri(text) or text
|
||||
real = real.strip()
|
||||
if not real:
|
||||
return None
|
||||
# If it's already a bare hydrus hash, use it directly
|
||||
lower_real = real.lower()
|
||||
if re.fullmatch(r"[0-9a-f]{64}", lower_real):
|
||||
return lower_real
|
||||
|
||||
# If it's a hydrus file URL, normalize to the hash for dedupe
|
||||
try:
|
||||
parsed = urlparse(real)
|
||||
if parsed.scheme in {"http", "https", "hydrus"}:
|
||||
if parsed.path.endswith("/get_files/file"):
|
||||
qs = parse_qs(parsed.query)
|
||||
h = qs.get("hash", [None])[0]
|
||||
if h and re.fullmatch(r"[0-9a-f]{64}", h.lower()):
|
||||
return h.lower()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Normalize slashes for Windows paths and lowercase for comparison
|
||||
real = real.replace('\\', '\\')
|
||||
real = real.replace('\\', '\\')
|
||||
return real.lower()
|
||||
|
||||
|
||||
def _infer_store_from_playlist_item(item: Dict[str, Any]) -> str:
|
||||
"""Infer a friendly store label from an MPV playlist entry."""
|
||||
name = item.get("filename") if isinstance(item, dict) else None
|
||||
@@ -97,6 +128,10 @@ def _infer_store_from_playlist_item(item: Dict[str, Any]) -> str:
|
||||
if memory_target:
|
||||
target = memory_target
|
||||
|
||||
# Hydrus hashes: bare 64-hex entries
|
||||
if re.fullmatch(r"[0-9a-f]{64}", target.lower()):
|
||||
return "hydrus"
|
||||
|
||||
lower = target.lower()
|
||||
if lower.startswith("magnet:"):
|
||||
return "magnet"
|
||||
@@ -245,31 +280,36 @@ def _monitor_mpv_logs(duration: float = 3.0) -> None:
|
||||
# Request log messages
|
||||
client.send_command({"command": ["request_log_messages", "warn"]})
|
||||
|
||||
# On Windows named pipes, avoid blocking the CLI; skip log read entirely
|
||||
if client.is_windows:
|
||||
client.disconnect()
|
||||
return
|
||||
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# Unix sockets already have timeouts set; read until duration expires
|
||||
while time.time() - start_time < duration:
|
||||
# We need to read raw lines from the socket
|
||||
if client.is_windows:
|
||||
try:
|
||||
line = client.sock.readline()
|
||||
if line:
|
||||
try:
|
||||
msg = json.loads(line)
|
||||
if msg.get("event") == "log-message":
|
||||
text = msg.get("text", "").strip()
|
||||
prefix = msg.get("prefix", "")
|
||||
level = msg.get("level", "")
|
||||
if "ytdl" in prefix or level == "error":
|
||||
debug(f"[MPV {prefix}] {text}", file=sys.stderr)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except Exception:
|
||||
break
|
||||
else:
|
||||
# Unix socket handling (simplified)
|
||||
try:
|
||||
chunk = client.sock.recv(4096)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
if not chunk:
|
||||
break
|
||||
for line in chunk.decode("utf-8", errors="ignore").splitlines():
|
||||
try:
|
||||
msg = json.loads(line)
|
||||
if msg.get("event") == "log-message":
|
||||
text = msg.get("text", "").strip()
|
||||
prefix = msg.get("prefix", "")
|
||||
level = msg.get("level", "")
|
||||
if "ytdl" in prefix or level == "error":
|
||||
debug(f"[MPV {prefix}] {text}", file=sys.stderr)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -294,6 +334,31 @@ def _queue_items(items: List[Any], clear_first: bool = False, config: Optional[D
|
||||
except Exception:
|
||||
hydrus_url = None
|
||||
|
||||
# Dedupe existing playlist before adding more (unless we're replacing it)
|
||||
existing_targets: set[str] = set()
|
||||
if not clear_first:
|
||||
playlist = _get_playlist(silent=True) or []
|
||||
dup_indexes: List[int] = []
|
||||
for idx, pl_item in enumerate(playlist):
|
||||
fname = pl_item.get("filename") if isinstance(pl_item, dict) else str(pl_item)
|
||||
alt = pl_item.get("playlist-path") if isinstance(pl_item, dict) else None
|
||||
norm = _normalize_playlist_target(fname) or _normalize_playlist_target(alt)
|
||||
if not norm:
|
||||
continue
|
||||
if norm in existing_targets:
|
||||
dup_indexes.append(idx)
|
||||
else:
|
||||
existing_targets.add(norm)
|
||||
|
||||
# Remove duplicates from playlist starting from the end to keep indices valid
|
||||
for idx in reversed(dup_indexes):
|
||||
try:
|
||||
_send_ipc_command({"command": ["playlist-remove", idx], "request_id": 106}, silent=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
new_targets: set[str] = set()
|
||||
|
||||
for i, item in enumerate(items):
|
||||
# Extract URL/Path
|
||||
target = None
|
||||
@@ -309,6 +374,16 @@ def _queue_items(items: List[Any], clear_first: bool = False, config: Optional[D
|
||||
target = item
|
||||
|
||||
if target:
|
||||
# If we just have a hydrus hash, build a direct file URL for MPV
|
||||
if re.fullmatch(r"[0-9a-f]{64}", str(target).strip().lower()) and hydrus_url:
|
||||
target = f"{hydrus_url.rstrip('/')}/get_files/file?hash={str(target).strip()}"
|
||||
|
||||
norm_key = _normalize_playlist_target(target) or str(target).strip().lower()
|
||||
if norm_key in existing_targets or norm_key in new_targets:
|
||||
debug(f"Skipping duplicate playlist entry: {title or target}")
|
||||
continue
|
||||
new_targets.add(norm_key)
|
||||
|
||||
# Check if it's a yt-dlp supported URL
|
||||
is_ytdlp = False
|
||||
if target.startswith("http") and is_url_supported_by_ytdlp(target):
|
||||
@@ -699,7 +774,11 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
# Monitor logs briefly for errors (e.g. ytdl failures)
|
||||
_monitor_mpv_logs(3.0)
|
||||
return 0
|
||||
|
||||
# Refresh playlist view so the user sees the new current item immediately
|
||||
items = _get_playlist(silent=True) or items
|
||||
list_mode = True
|
||||
index_arg = None
|
||||
else:
|
||||
debug(f"Failed to play item: {resp.get('error') if resp else 'No response'}")
|
||||
return 1
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Worker cmdlet: Display workers table in ResultTable format."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Sequence, List
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Sequence, List
|
||||
|
||||
from cmdlets import register
|
||||
from cmdlets._shared import Cmdlet, CmdletArg
|
||||
@@ -12,6 +13,9 @@ import pipeline as ctx
|
||||
from helper.logger import log
|
||||
from config import get_local_storage_path
|
||||
|
||||
DEFAULT_LIMIT = 100
|
||||
WORKER_STATUS_FILTERS = {"running", "completed", "error", "cancelled"}
|
||||
HELP_FLAGS = {"-?", "/?", "--help", "-h", "help", "--cmdlet"}
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".worker",
|
||||
@@ -21,6 +25,8 @@ CMDLET = Cmdlet(
|
||||
CmdletArg("status", description="Filter by status: running, completed, error (default: all)"),
|
||||
CmdletArg("limit", type="integer", description="Limit results (default: 100)"),
|
||||
CmdletArg("@N", description="Select worker by index (1-based) and display full logs"),
|
||||
CmdletArg("-id", description="Show full logs for a specific worker"),
|
||||
CmdletArg("-clear", type="flag", description="Remove completed workers from the database"),
|
||||
],
|
||||
details=[
|
||||
"- Shows all background worker tasks and their output",
|
||||
@@ -37,284 +43,285 @@ CMDLET = Cmdlet(
|
||||
)
|
||||
|
||||
|
||||
def _has_help_flag(args_list: Sequence[str]) -> bool:
|
||||
return any(str(arg).lower() in HELP_FLAGS for arg in args_list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkerCommandOptions:
|
||||
status: str | None = None
|
||||
limit: int = DEFAULT_LIMIT
|
||||
worker_id: str | None = None
|
||||
clear: bool = False
|
||||
|
||||
|
||||
@register([".worker", "worker", "workers"])
|
||||
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Display workers table or show detailed logs for a specific worker."""
|
||||
args_list = [str(arg) for arg in (args or [])]
|
||||
selection_indices = ctx.get_last_selection()
|
||||
selection_requested = bool(selection_indices) and isinstance(result, list) and len(result) > 0
|
||||
"""Display workers table or show detailed logs for a specific worker."""
|
||||
args_list = [str(arg) for arg in (args or [])]
|
||||
selection_indices = ctx.get_last_selection()
|
||||
selection_requested = bool(selection_indices) and isinstance(result, list) and len(result) > 0
|
||||
|
||||
# Parse arguments for list view
|
||||
status_filter: str | None = None
|
||||
limit = 100
|
||||
clear_requested = False
|
||||
worker_id_arg: str | None = None
|
||||
i = 0
|
||||
while i < len(args_list):
|
||||
arg = args_list[i]
|
||||
low = arg.lower()
|
||||
if low in {"-limit", "--limit"} and i + 1 < len(args_list):
|
||||
try:
|
||||
limit = max(1, int(args_list[i + 1]))
|
||||
except ValueError:
|
||||
limit = 100
|
||||
i += 2
|
||||
elif low in {"-id", "--id"} and i + 1 < len(args_list):
|
||||
worker_id_arg = args_list[i + 1]
|
||||
i += 2
|
||||
elif low in {"-clear", "--clear"}:
|
||||
clear_requested = True
|
||||
i += 1
|
||||
elif low in {"running", "completed", "error", "cancelled"}:
|
||||
status_filter = low
|
||||
i += 1
|
||||
elif not arg.startswith("-"):
|
||||
status_filter = low
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
if _has_help_flag(args_list):
|
||||
log(json.dumps(CMDLET, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
try:
|
||||
if any(str(a).lower() in {"-?", "/?", "--help", "-h", "help", "--cmdlet"} for a in args):
|
||||
log(json.dumps(CMDLET, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
except Exception:
|
||||
pass
|
||||
options = _parse_worker_args(args_list)
|
||||
|
||||
library_root = get_local_storage_path(config or {})
|
||||
if not library_root:
|
||||
log("No library root configured", file=sys.stderr)
|
||||
return 1
|
||||
library_root = get_local_storage_path(config or {})
|
||||
if not library_root:
|
||||
log("No library root configured", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
from helper.local_library import LocalLibraryDB
|
||||
with LocalLibraryDB(library_root) as db:
|
||||
if clear_requested:
|
||||
count = db.clear_finished_workers()
|
||||
log(f"Cleared {count} finished workers.")
|
||||
return 0
|
||||
|
||||
if worker_id_arg:
|
||||
worker = db.get_worker(worker_id_arg)
|
||||
if worker:
|
||||
events = []
|
||||
try:
|
||||
wid = worker.get("worker_id")
|
||||
if wid and hasattr(db, "get_worker_events"):
|
||||
events = db.get_worker_events(wid)
|
||||
except Exception:
|
||||
pass
|
||||
_emit_worker_detail(worker, events)
|
||||
return 0
|
||||
else:
|
||||
log(f"Worker not found: {worker_id_arg}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
from helper.local_library import LocalLibraryDB
|
||||
|
||||
if selection_requested:
|
||||
return _render_worker_selection(db, result)
|
||||
return _render_worker_list(db, status_filter, limit)
|
||||
except Exception as exc:
|
||||
log(f"Workers query failed: {exc}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
return 1
|
||||
with LocalLibraryDB(library_root) as db:
|
||||
if options.clear:
|
||||
count = db.clear_finished_workers()
|
||||
log(f"Cleared {count} finished workers.")
|
||||
return 0
|
||||
|
||||
if options.worker_id:
|
||||
worker = db.get_worker(options.worker_id)
|
||||
if worker:
|
||||
events: List[Dict[str, Any]] = []
|
||||
try:
|
||||
wid = worker.get("worker_id")
|
||||
if wid and hasattr(db, "get_worker_events"):
|
||||
events = db.get_worker_events(wid)
|
||||
except Exception:
|
||||
pass
|
||||
_emit_worker_detail(worker, events)
|
||||
return 0
|
||||
log(f"Worker not found: {options.worker_id}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if selection_requested:
|
||||
return _render_worker_selection(db, result)
|
||||
|
||||
return _render_worker_list(db, options.status, options.limit)
|
||||
except Exception as exc:
|
||||
log(f"Workers query failed: {exc}", file=sys.stderr)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def _parse_worker_args(args_list: Sequence[str]) -> WorkerCommandOptions:
|
||||
options = WorkerCommandOptions()
|
||||
i = 0
|
||||
while i < len(args_list):
|
||||
arg = args_list[i]
|
||||
low = arg.lower()
|
||||
if low in {"-limit", "--limit"} and i + 1 < len(args_list):
|
||||
options.limit = _normalize_limit(args_list[i + 1])
|
||||
i += 2
|
||||
elif low in {"-id", "--id"} and i + 1 < len(args_list):
|
||||
options.worker_id = args_list[i + 1]
|
||||
i += 2
|
||||
elif low in {"-clear", "--clear"}:
|
||||
options.clear = True
|
||||
i += 1
|
||||
elif low in {"-status", "--status"} and i + 1 < len(args_list):
|
||||
options.status = args_list[i + 1].lower()
|
||||
i += 2
|
||||
elif low in WORKER_STATUS_FILTERS:
|
||||
options.status = low
|
||||
i += 1
|
||||
elif not arg.startswith("-"):
|
||||
options.status = low
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return options
|
||||
|
||||
|
||||
def _normalize_limit(value: Any) -> int:
|
||||
try:
|
||||
return max(1, int(value))
|
||||
except (TypeError, ValueError):
|
||||
return DEFAULT_LIMIT
|
||||
|
||||
|
||||
def _render_worker_list(db, status_filter: str | None, limit: int) -> int:
|
||||
workers = db.get_all_workers(limit=limit)
|
||||
if status_filter:
|
||||
workers = [w for w in workers if str(w.get("status", "")).lower() == status_filter]
|
||||
workers = db.get_all_workers(limit=limit)
|
||||
if status_filter:
|
||||
workers = [w for w in workers if str(w.get("status", "")).lower() == status_filter]
|
||||
|
||||
if not workers:
|
||||
log("No workers found", file=sys.stderr)
|
||||
return 0
|
||||
if not workers:
|
||||
log("No workers found", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
for worker in workers:
|
||||
started = worker.get("started_at", "")
|
||||
ended = worker.get("completed_at", worker.get("last_updated", ""))
|
||||
|
||||
date_str = _extract_date(started)
|
||||
start_time = _format_event_timestamp(started)
|
||||
end_time = _format_event_timestamp(ended)
|
||||
|
||||
item = {
|
||||
"columns": [
|
||||
("Status", worker.get("status", "")),
|
||||
("Pipe", _summarize_pipe(worker.get("pipe"))),
|
||||
("Date", date_str),
|
||||
("Start Time", start_time),
|
||||
("End Time", end_time),
|
||||
],
|
||||
"__worker_metadata": worker,
|
||||
"_selection_args": ["-id", worker.get("worker_id")]
|
||||
}
|
||||
ctx.emit(item)
|
||||
return 0
|
||||
for worker in workers:
|
||||
started = worker.get("started_at", "")
|
||||
ended = worker.get("completed_at", worker.get("last_updated", ""))
|
||||
|
||||
date_str = _extract_date(started)
|
||||
start_time = _format_event_timestamp(started)
|
||||
end_time = _format_event_timestamp(ended)
|
||||
|
||||
item = {
|
||||
"columns": [
|
||||
("Status", worker.get("status", "")),
|
||||
("Pipe", _summarize_pipe(worker.get("pipe"))),
|
||||
("Date", date_str),
|
||||
("Start Time", start_time),
|
||||
("End Time", end_time),
|
||||
],
|
||||
"__worker_metadata": worker,
|
||||
"_selection_args": ["-id", worker.get("worker_id")],
|
||||
}
|
||||
ctx.emit(item)
|
||||
return 0
|
||||
|
||||
|
||||
def _render_worker_selection(db, selected_items: Any) -> int:
|
||||
if not isinstance(selected_items, list):
|
||||
log("Selection payload missing", file=sys.stderr)
|
||||
return 1
|
||||
if not isinstance(selected_items, list):
|
||||
log("Selection payload missing", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
emitted = False
|
||||
for item in selected_items:
|
||||
worker = _resolve_worker_record(db, item)
|
||||
if not worker:
|
||||
continue
|
||||
events = []
|
||||
try:
|
||||
events = db.get_worker_events(worker.get("worker_id")) if hasattr(db, "get_worker_events") else []
|
||||
except Exception:
|
||||
events = []
|
||||
_emit_worker_detail(worker, events)
|
||||
emitted = True
|
||||
if not emitted:
|
||||
log("Selected rows no longer exist", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
emitted = False
|
||||
for item in selected_items:
|
||||
worker = _resolve_worker_record(db, item)
|
||||
if not worker:
|
||||
continue
|
||||
events: List[Dict[str, Any]] = []
|
||||
try:
|
||||
events = db.get_worker_events(worker.get("worker_id")) if hasattr(db, "get_worker_events") else []
|
||||
except Exception:
|
||||
events = []
|
||||
_emit_worker_detail(worker, events)
|
||||
emitted = True
|
||||
if not emitted:
|
||||
log("Selected rows no longer exist", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _resolve_worker_record(db, payload: Any) -> Dict[str, Any] | None:
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
worker_data = payload.get("__worker_metadata")
|
||||
worker_id = None
|
||||
if isinstance(worker_data, dict):
|
||||
worker_id = worker_data.get("worker_id")
|
||||
else:
|
||||
worker_id = payload.get("worker_id")
|
||||
worker_data = None
|
||||
if worker_id:
|
||||
fresh = db.get_worker(worker_id)
|
||||
if fresh:
|
||||
return fresh
|
||||
return worker_data if isinstance(worker_data, dict) else None
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
worker_data = payload.get("__worker_metadata")
|
||||
worker_id = None
|
||||
if isinstance(worker_data, dict):
|
||||
worker_id = worker_data.get("worker_id")
|
||||
else:
|
||||
worker_id = payload.get("worker_id")
|
||||
worker_data = None
|
||||
if worker_id:
|
||||
fresh = db.get_worker(worker_id)
|
||||
if fresh:
|
||||
return fresh
|
||||
return worker_data if isinstance(worker_data, dict) else None
|
||||
|
||||
|
||||
def _emit_worker_detail(worker: Dict[str, Any], events: List[Dict[str, Any]]) -> None:
|
||||
# Parse stdout logs into rows
|
||||
stdout_content = worker.get("stdout", "") or ""
|
||||
|
||||
# Try to parse lines if they follow the standard log format
|
||||
# Format: YYYY-MM-DD HH:MM:SS - name - level - message
|
||||
lines = stdout_content.splitlines()
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Default values
|
||||
timestamp = ""
|
||||
level = "INFO"
|
||||
message = line
|
||||
|
||||
# Try to parse standard format
|
||||
try:
|
||||
parts = line.split(" - ", 3)
|
||||
if len(parts) >= 4:
|
||||
# Full format
|
||||
ts_str, _, lvl, msg = parts
|
||||
timestamp = _format_event_timestamp(ts_str)
|
||||
level = lvl
|
||||
message = msg
|
||||
elif len(parts) == 3:
|
||||
# Missing name or level
|
||||
ts_str, lvl, msg = parts
|
||||
timestamp = _format_event_timestamp(ts_str)
|
||||
level = lvl
|
||||
message = msg
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
item = {
|
||||
"columns": [
|
||||
("Time", timestamp),
|
||||
("Level", level),
|
||||
("Message", message)
|
||||
]
|
||||
}
|
||||
ctx.emit(item)
|
||||
|
||||
# Also emit events if available and not redundant
|
||||
# (For now, just focusing on stdout logs as requested)
|
||||
stdout_content = worker.get("stdout", "") or ""
|
||||
|
||||
lines = stdout_content.splitlines()
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
timestamp = ""
|
||||
level = "INFO"
|
||||
message = line
|
||||
|
||||
try:
|
||||
parts = line.split(" - ", 3)
|
||||
if len(parts) >= 4:
|
||||
ts_str, _, lvl, msg = parts
|
||||
timestamp = _format_event_timestamp(ts_str)
|
||||
level = lvl
|
||||
message = msg
|
||||
elif len(parts) == 3:
|
||||
ts_str, lvl, msg = parts
|
||||
timestamp = _format_event_timestamp(ts_str)
|
||||
level = lvl
|
||||
message = msg
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
item = {
|
||||
"columns": [
|
||||
("Time", timestamp),
|
||||
("Level", level),
|
||||
("Message", message),
|
||||
]
|
||||
}
|
||||
ctx.emit(item)
|
||||
|
||||
# Events are already always derived from stdout for now.
|
||||
|
||||
|
||||
def _summarize_pipe(pipe_value: Any, limit: int = 60) -> str:
|
||||
text = str(pipe_value or "").strip()
|
||||
if not text:
|
||||
return "(none)"
|
||||
return text if len(text) <= limit else text[: limit - 3] + "..."
|
||||
text = str(pipe_value or "").strip()
|
||||
if not text:
|
||||
return "(none)"
|
||||
return text if len(text) <= limit else text[: limit - 3] + "..."
|
||||
|
||||
|
||||
def _format_event_timestamp(raw_timestamp: Any) -> str:
|
||||
dt = _parse_to_local(raw_timestamp)
|
||||
if dt:
|
||||
return dt.strftime("%H:%M:%S")
|
||||
dt = _parse_to_local(raw_timestamp)
|
||||
if dt:
|
||||
return dt.strftime("%H:%M:%S")
|
||||
|
||||
if not raw_timestamp:
|
||||
return "--:--:--"
|
||||
text = str(raw_timestamp)
|
||||
if "T" in text:
|
||||
time_part = text.split("T", 1)[1]
|
||||
elif " " in text:
|
||||
time_part = text.split(" ", 1)[1]
|
||||
else:
|
||||
time_part = text
|
||||
return time_part[:8] if len(time_part) >= 8 else time_part
|
||||
if not raw_timestamp:
|
||||
return "--:--:--"
|
||||
text = str(raw_timestamp)
|
||||
if "T" in text:
|
||||
time_part = text.split("T", 1)[1]
|
||||
elif " " in text:
|
||||
time_part = text.split(" ", 1)[1]
|
||||
else:
|
||||
time_part = text
|
||||
return time_part[:8] if len(time_part) >= 8 else time_part
|
||||
|
||||
|
||||
def _parse_to_local(timestamp_str: Any) -> datetime | None:
|
||||
if not timestamp_str:
|
||||
return None
|
||||
text = str(timestamp_str).strip()
|
||||
if not text:
|
||||
return None
|
||||
if not timestamp_str:
|
||||
return None
|
||||
text = str(timestamp_str).strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Check for T separator (Python isoformat - Local time)
|
||||
if 'T' in text:
|
||||
return datetime.fromisoformat(text)
|
||||
|
||||
# Check for space separator (SQLite CURRENT_TIMESTAMP - UTC)
|
||||
# Format: YYYY-MM-DD HH:MM:SS
|
||||
if ' ' in text:
|
||||
# Assume UTC
|
||||
dt = datetime.strptime(text, "%Y-%m-%d %H:%M:%S")
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone() # Convert to local
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
try:
|
||||
if "T" in text:
|
||||
return datetime.fromisoformat(text)
|
||||
if " " in text:
|
||||
dt = datetime.strptime(text, "%Y-%m-%d %H:%M:%S")
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _extract_date(raw_timestamp: Any) -> str:
|
||||
dt = _parse_to_local(raw_timestamp)
|
||||
if dt:
|
||||
return dt.strftime("%m-%d-%y")
|
||||
|
||||
# Fallback
|
||||
if not raw_timestamp:
|
||||
return ""
|
||||
text = str(raw_timestamp)
|
||||
# Extract YYYY-MM-DD part
|
||||
date_part = ""
|
||||
if "T" in text:
|
||||
date_part = text.split("T", 1)[0]
|
||||
elif " " in text:
|
||||
date_part = text.split(" ", 1)[0]
|
||||
else:
|
||||
date_part = text
|
||||
|
||||
# Convert YYYY-MM-DD to MM-DD-YY
|
||||
try:
|
||||
parts = date_part.split("-")
|
||||
if len(parts) == 3:
|
||||
year, month, day = parts
|
||||
return f"{month}-{day}-{year[2:]}"
|
||||
except Exception:
|
||||
pass
|
||||
return date_part
|
||||
dt = _parse_to_local(raw_timestamp)
|
||||
if dt:
|
||||
return dt.strftime("%m-%d-%y")
|
||||
|
||||
if not raw_timestamp:
|
||||
return ""
|
||||
text = str(raw_timestamp)
|
||||
date_part = ""
|
||||
if "T" in text:
|
||||
date_part = text.split("T", 1)[0]
|
||||
elif " " in text:
|
||||
date_part = text.split(" ", 1)[0]
|
||||
else:
|
||||
date_part = text
|
||||
|
||||
try:
|
||||
parts = date_part.split("-")
|
||||
if len(parts) == 3:
|
||||
year, month, day = parts
|
||||
return f"{month}-{day}-{year[2:]}"
|
||||
except Exception:
|
||||
pass
|
||||
return date_part
|
||||
|
||||
Reference in New Issue
Block a user