mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-24 16:52:28 +02:00
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
209 lines
7.5 KiB
Bash
Executable File
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
|