Files
tuliprox/bin/dvr_doctor.sh
T
euzuandGitHub 16d32d1298 DVR Feature (#819)
feat: complete DVR and improve streaming, security, configuration, and UI

Complete the Digital Video Recorder subsystem and add a broad set of
reliability, security, streaming, configuration, processing, and Web UI
improvements across Tuliprox.

DVR:

* complete live recording and provider-aware VOD download support
* add recording queue, workers, scheduling, and recurring recording rules
* add conflict detection and capacity-aware scheduling
* add pause, resume, retry, edit, cancel, and delete workflows
* add recording quotas and configurable retention policies
* add crash recovery and startup reconciliation
* add durable lifecycle notifications with per-channel retries
* add DVR health monitoring and diagnostic tooling
* add secure access to recordings, thumbnails, and subtitles
* add WebSocket notifications for recording and rule changes
* add Web UI management for recordings, rules, progress, and task state
* add RBAC, configuration, documentation, and i18n support

Streaming and HLS:

* fix shared-stream idle handling and release dead provider streams correctly
* stop tee streams when both client and cache consumers are gone
* cancel provisioning probes when client streams terminate
* fix transient HLS origin work accounting and intermittent 503 responses
* make stream buffer byte limits configurable
* make shared subscriber idle timeout configurable
* make initial HLS manifest wait timeout configurable
* add configurable TS chunk packet count
* add configurable HLS refresh failure backoff
* centralize redirect limits and retry jitter handling
* improve provider DNS refresh behavior and failover tuning
* preserve UTF-8 characters in catchup templates
* improve stream history validation and persistence error handling

Security:

* use constant-time credential comparisons
* harden library and media path handling against traversal and symlink escapes
* only trust forwarded client IP headers from configured trusted proxies
* redact credentials and sensitive URL data from logs
* reject invalid authentication status-code configuration
* deny users with unresolved plans or invalid content filters
* improve authentication error handling across proxy and HLS endpoints

Configuration and reliability:

* prevent invalid api-proxy.yml reloads from terminating the running server
* fully validate API proxy configuration before persisting changes
* log configuration and EPG cleanup failures instead of silently discarding them
* keep the last valid configuration active after failed hot reloads
* align backend and shared media-server validation
* remove duplicated path and normalization logic
* improve DNS-store recovery and Windows rename fallback handling
* reject invalid duration, timestamp, and numeric conversions safely
* fix playlist bouquet save error handling
* fix provider record update detection
* fix cache boundary handling
* improve startup and persistence failure diagnostics

Filtering, search, sorting, and processing:

* add field-scoped playlist explorer search
* centralize shared stream-history search field definitions
* extend the filter DSL with string, set, and numeric operators
* add EPG ID, channel number, and detected quality as filterable fields
* add filter dry-run preview API with match statistics and samples
* report filter syntax errors with line and column information
* add natural numeric-aware sorting
* add quality-aware channel deduplication
* add accent-independent deduplication
* move natural sorting and quality detection helpers into shared code
* persist explorer search-field selection across reloads

User plans and content access:

* add reusable API user plans for capability tiers
* support inherited cluster and connection limits with per-user overrides
* add plan-level and user-level content filters
* enforce content filters across Xtream, M3U, direct playback, resource access,
  stream info, short EPG, categories, and XMLTV
* add trial plans with automatic expiry and Trial status
* add plan selection and content filtering to the user editor
* add full plan management to the API configuration Web UI
* migrate the API user database to schema V7 with plan and filter persistence

Web UI and accessibility:

* add live logging console to the stats page
* improve login error handling and prevent duplicate authentication requests
* add keyboard navigation to tabs, menus, tables, and search
* add ARIA roles, labels, validation state, and live-region feedback
* add confirmation dialogs for destructive actions
* add unsaved-change warnings and Ctrl/Cmd+S shortcuts
* add loading, progress, empty, and in-flight states across views
* improve dropdown and single-selection behavior
* add clipboard and credential-copy helpers
* persist table pagination and explorer search preferences
* improve error recovery when UI context providers are unavailable
* remove multiple panic-prone unwrap and browser API paths
* replace remaining hardcoded UI strings with translation keys

Maintenance:

* resolve backend and frontend compiler and Clippy warnings
* update packages and test fixtures
* consolidate duplicated helpers and validation logic
* improve documentation for configuration, filters, plans, DVR, and REST APIs
* add and update tests for migrations, filters, deduplication, sorting,
  configuration, streaming, and accessibility behavior
2026-08-21 14:50:12 +02:00

209 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# DVR support diagnostics.
#
# Collects the DVR's runtime state into one readable dump for a support
# ticket: supervisor liveness, the effective configuration, and the
# on-disk artefacts the recording feature owns. Read-only — it never
# mutates config, the queue, or a recording.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
WORKING_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
die() {
echo "🧨 Error: $*" >&2
exit 1
}
usage() {
cat <<'USAGE'
Usage: dvr_doctor.sh [--url URL] [--token TOKEN] [--storage-dir DIR]
--url Base URL of the running server. Default: http://localhost:8901
--token Bearer token for an administrator. Without it the health
section is skipped; the on-disk sections still work.
--storage-dir Server storage directory, for the on-disk sections.
Default: $TULIPROX_HOME/data, else ./data
Environment: TULIPROX_URL, TULIPROX_TOKEN, TULIPROX_HOME.
USAGE
}
URL="${TULIPROX_URL:-http://localhost:8901}"
TOKEN="${TULIPROX_TOKEN:-}"
STORAGE_DIR="${TULIPROX_HOME:+${TULIPROX_HOME}/data}"
while [[ $# -gt 0 ]]; do
case "$1" in
--url) URL="${2:?--url needs a value}"; shift 2 ;;
--token) TOKEN="${2:?--token needs a value}"; shift 2 ;;
--storage-dir) STORAGE_DIR="${2:?--storage-dir needs a value}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done
STORAGE_DIR="${STORAGE_DIR:-${WORKING_DIR}/data}"
command -v curl >/dev/null 2>&1 || die "curl is required"
# jq is optional: without it the JSON is emitted raw rather than pretty.
if command -v jq >/dev/null 2>&1; then
pretty() { jq . 2>/dev/null || cat; }
else
pretty() { cat; }
fi
section() {
printf '\n=== %s ===\n' "$1"
}
fetch() {
# $1 = path. Prints the body; returns non-zero on a transport failure.
local path="$1"
if [[ -n "${TOKEN}" ]]; then
# Token goes through stdin (curl `-H @-`) so it does not appear in
# the process listing (`ps`, `/proc/<pid>/cmdline`).
curl -fsS -H @- "${URL}${path}" <<<"Authorization: Bearer ${TOKEN}"
else
curl -fsS "${URL}${path}"
fi
}
printf 'Tuliprox DVR diagnostics\n'
printf 'generated: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf 'url: %s\n' "${URL}"
printf 'storage dir: %s\n' "${STORAGE_DIR}"
printf 'token: %s\n' "$([[ -n "${TOKEN}" ]] && echo provided || echo '(none — health section skipped)')"
section "Supervisor health"
if [[ -z "${TOKEN}" ]]; then
echo "skipped: needs an administrator token (--token)"
else
if ! fetch /api/v1/recording/health | pretty; then
echo "unavailable: the endpoint refused or the server is down."
echo "A 403 means the token is not an administrator's."
echo "A 404 means the build predates the health endpoint."
echo "A 501 means recording.enabled is false."
fi
fi
section "Effective recording configuration"
if [[ -z "${TOKEN}" ]]; then
echo "skipped: needs a token (--token)"
elif command -v jq >/dev/null 2>&1; then
# The DVR block only; the rest of the config may contain credentials.
fetch /api/v1/config \
| jq '.config.video.download.recording // "no recording block configured (defaults apply)"' \
|| echo "unavailable"
else
echo "skipped: needs jq to extract the recording block without dumping"
echo "the whole config, which may contain credentials."
fi
section "Recording quota"
if [[ -z "${TOKEN}" ]]; then
echo "skipped: needs a token (--token)"
else
fetch /api/v1/recording/quota | pretty || echo "unavailable"
fi
section "On-disk state"
queue_file="${STORAGE_DIR}/downloads_state.json"
rules_file="${STORAGE_DIR}/recording_rules.json"
outbox_file="${STORAGE_DIR}/recording_notification_outbox.json"
# mtime_as_iso8601_utc <file>
# Prints the file's modification time as `YYYY-MM-DDTHH:MM:SSZ`, or
# `unknown` if neither `stat` flavour works. GNU `stat -c %Y` and BSD
# / macOS `stat -f %m` both yield an mtime epoch; GNU `date -d @N`
# accepts the `@`-prefixed epoch and formats it, BSD/macOS `date -r
# N` accepts the raw epoch directly (no `@` prefix).
mtime_as_iso8601_utc() {
local f="$1" ts
if ts=$(stat -c %Y -- "${f}" 2>/dev/null); then
date -u -d "@${ts}" '+%Y-%m-%dT%H:%M:%SZ'
elif ts=$(stat -f %m -- "${f}" 2>/dev/null); then
# BSD/macOS `date -r` reads the raw epoch as a numeric argument;
# the `@N` form is GNU-only and would fail with "No such file or
# directory" because date would try to stat a file named `@<ts>`.
date -u -r "${ts}" '+%Y-%m-%dT%H:%M:%SZ'
else
echo unknown
fi
}
for f in "${queue_file}" "${rules_file}" "${outbox_file}"; do
if [[ -f "${f}" ]]; then
printf '%s (%s bytes, modified %s)\n' \
"${f}" \
"$(wc -c <"${f}" | tr -d ' ')" \
"$(mtime_as_iso8601_utc "${f}")"
else
printf '%s (absent)\n' "${f}"
fi
done
if [[ -f "${queue_file}" ]] && command -v jq >/dev/null 2>&1; then
section "Queue summary (no titles, no paths)"
# Aggregate only. Titles, filenames, and owner ids are deliberately not
# printed: a diagnostics dump gets pasted into tickets.
jq '{
revision: .revision,
queued: (.queue | length),
scheduled: (.scheduled | length),
active: (if .active then 1 else 0 end),
finished: (.finished | length),
recordings_by_state:
([.queue[], .scheduled[], .finished[]]
+ (if .active then [.active] else [] end)
| map(select(.recording != null))
| group_by(.state)
| map({ (.[0].state | tostring): length })
| add // {}),
stuck_deleting:
([.queue[], .scheduled[], .finished[]]
+ (if .active then [.active] else [] end)
| map(select(.recording != null and .recording.deleting_previous_state != null))
| length)
}' "${queue_file}" || echo "could not parse ${queue_file}"
fi
if [[ -f "${rules_file}" ]] && command -v jq >/dev/null 2>&1; then
section "Rules summary"
jq '{
version: .version,
rules_total: (.rules | length),
rules_enabled: (.rules | map(select(.enabled)) | length),
new_episode_enabled:
(.rules | map(select(.enabled and (.body | has("NewEpisode")))) | length),
weekly_enabled:
(.rules | map(select(.enabled and (.body | has("WeeklyTimeslot")))) | length),
tombstones: (.tombstones.tombstones | length)
}' "${rules_file}" || echo "could not parse ${rules_file}"
echo
echo "Note: enabled NewEpisode rules cannot currently match — the scheduler"
echo "has no EPG horizon, so only WeeklyTimeslot rules materialize."
fi
if [[ -f "${outbox_file}" ]] && command -v jq >/dev/null 2>&1; then
section "Notification outbox"
jq '{ next_id: .next_id, pending: (.entries | length),
attempts: (.entries | map(.attempts)),
channels_pending: (.entries | map(.pending) | flatten | unique) }' \
"${outbox_file}" || echo "could not parse ${outbox_file}"
fi
section "What to look at first"
cat <<'HINTS'
- retention_last_tick older than disk.cleanup_interval_secs → the retention
supervisor is stalled or the DVR is disabled.
- reconciliation_last_run null → the supervisors never started; check the log
for "Recording is disabled" or a startup error.
- stuck_deleting > 0 after a restart → reconciliation did not clear a task;
grep the log for recording_reconciliation_unsafe_path.
- notification outbox pending > 0 and not draining → grep the log for
recording_notification_dead_lettered.
- no retention, no watermarks and no quota → recording disk use is unbounded;
the server logs a warning for this at startup.
HINTS