This document describes the runtime structure of hdhriptv, how major
data paths move through the system, and exactly where public/admin routes are
implemented in code.
cmd/hdhriptv/main.go wires all subsystems and starts:
- HTTP server(s) for HDHR endpoints, admin UI/API,
/healthz, and optional/metrics - UDP discovery server on port
65001 - optional UPnP/SSDP responder on UDP
1900 - automation scheduler and asynchronous job runner
- optional background source prober
Core subsystem ownership:
- Playlist ingest and catalog persistence
internal/playlist/manager.gointernal/playlist/refresh.gointernal/m3u/parser.gointernal/store/sqlite/catalog_index.gointernal/store/sqlite/playlist_sources.go
- Published channels and source graph
internal/channels/service.gointernal/store/sqlite/channels.go
- Stream session manager and tuner leasing
internal/stream/handler.gointernal/stream/shared_session.gointernal/stream/pump.gointernal/stream/ring.gointernal/stream/tuners.gointernal/stream/virtual_tuners.gointernal/stream/tuner_usage.gointernal/stream/ffmpeg.go
- Automation jobs and schedules
internal/jobs/runner.gointernal/jobs/playlist_sync.gointernal/jobs/auto_prioritize.gointernal/scheduler/scheduler.gointernal/store/sqlite/job_runs.gointernal/store/sqlite/metrics.go
- DVR integration
internal/dvr/service.gointernal/dvr/channels_provider.gointernal/dvr/jellyfin_provider.gointernal/store/sqlite/dvr.go
- HTTP surfaces
- Public HDHR:
internal/hdhr/http_handlers.go - UDP discovery:
internal/hdhr/discovery/udp.go - UPnP/SSDP discovery:
internal/hdhr/upnp/server.goandinternal/hdhr/upnp/protocol.go - Admin route registration/core wiring:
internal/http/admin_routes.go - Catalog/channel/source handlers:
internal/http/admin_channels.go - Dynamic channel and DVR lineup background workers:
internal/http/admin_workers.go - Tuner status/recovery and reverse-DNS cache handlers:
internal/http/admin_tuners.go - Automation admin handlers:
internal/http/admin_automation.go - DVR admin handlers:
internal/http/admin_dvr.go - Middleware:
internal/http/middleware/*.go
- Public HDHR:
Persistence is SQLite (internal/store/sqlite/store.go) with migrations under
internal/store/sqlite/migrations/.
- Trigger source:
- startup one-shot sync in
cmd/hdhriptv/main.go - manual trigger:
POST /api/admin/jobs/playlist-sync/run(optional?source_id=Nfor per-source sync) - scheduled trigger via
internal/scheduler/scheduler.go
- startup one-shot sync in
- Job runner starts persisted run in
internal/jobs/runner.go. internal/jobs/playlist_sync.goexecutes:- resolves the list of playlist sources to refresh (all enabled sources,
or a single source when
source_idis scoped via context) - refreshes each source via
internal/playlist/refresh.go:RefreshForSource(ctx, source)using bounded worker concurrency (playlist_sync_source_concurrency). Default worker count1preserves sequential behavior; higher values are opt-in and process sources in parallel while preserving per-source upsert/deactivation semantics and source-order result summaries - records per-source sync outcome (item count, duration, success/failure) and emits per-source Prometheus metrics
- reconcile channel sources via
internal/reconcile/reconcile.go(skipped when all sources fail) - optional post-sync DVR lineup reload hook (
DVRLineupReloader) viaReloadLineupForPlaylistSyncOutcome(...), with active-provider fan-out and provider-aware skip semantics for incomplete Jellyfin config; triggered when at least one source succeeds
- resolves the list of playlist sources to refresh (all enabled sources,
or a single source when
- Catalog refresh upserts active rows and marks unseen rows inactive
per source in
internal/store/sqlite/catalog_index.go— one failing source does not deactivate items from other sources.- IOERR diagnostics are enriched in
internal/store/sqlite/error_diag.go,internal/store/sqlite/ioerr_diag.go, andinternal/store/sqlite/op_trace.go:- one-shot
sqlite_ioerr_diag_bundlecaptures runtime pragma/db-file stats - optional
sqlite_ioerr_trace_dumpemits a rate-limited operation timeline from an in-memory fixed-size trace ring.
- one-shot
- IOERR diagnostics are enriched in
- Reconcile appends/synchronizes channel sources using
channels.Service.
internal/stream/handler.goresolves guide number and channel metadata.- Global tune backoff is checked before creating a new shared session.
SessionManagerininternal/stream/shared_session.gocreates/reuses one shared runtime session per channel.- Session acquires a tuner lease from the candidate source's virtual tuner
pool via
VirtualTunerManager(internal/stream/virtual_tuners.go) or the single globalPool(internal/stream/tuners.go) depending on configuration. Each playlist source has its own capacity-limited pool. - Pump publishes chunks into ring buffer (
internal/stream/pump.goandinternal/stream/ring.go) and subscribers stream the same shared bytes. - On failover to a source from a different playlist source, the session releases its current pool lease and acquires a new lease from the target source's pool — implementing cross-source virtual tuner migration.
- Source health, recovery cycle telemetry, and stall handling are updated in shared session logic.
- Trigger source:
- manual trigger:
POST /api/admin/jobs/auto-prioritize/run - scheduler callback
- manual trigger:
internal/jobs/auto_prioritize.go:- collects enabled channel sources
- reuses cached metrics where fresh (
stream_metrics) - probes pending sources via analyzer (
internal/analyzer/ffmpeg.go) - computes normalized per-channel ranking and reorders source priority
- Run status/progress/summary are persisted in
job_runs.
- Trigger source:
- manual trigger:
POST /api/admin/dvr/sync - scheduler callback for
dvr_lineup_sync - HTTP-triggered runs are detached from request cancellation and execute
under an internal timeout budget (
AdminHandler.dvrSyncTimeout, default2m)
- manual trigger:
internal/dvr/service.goexecutes a two-stage sync pipeline:buildSyncPlan: load mappings + provider lineup data, resolve station refs, compute patch diff/counters/warnings.applySyncPlan: apply provider patch and persist resolved station refs (or preview-only in dry-run mode).
SyncResultstores the per-lineup summaries, aggregate counters, warnings, and patch preview for API visibility and troubleshooting.- Provider scope note:
channelsprovider satisfies bothLineupReloadProviderandMappingProvider.jellyfinprovider satisfiesLineupReloadProvideronly.- sync/reverse-sync/test paths explicitly resolve
MappingProvider, while lineup reload paths resolveLineupReloadProvider.
- Config-shape note:
- DVR config normalization (provider/base-url/active-provider/sync-mode)
is centralized in
internal/dvr/config_normalize.goand reused by service/provider/store paths.
- DVR config normalization (provider/base-url/active-provider/sync-mode)
is centralized in
- Trigger source:
- global:
POST /api/admin/dvr/reverse-sync - per-channel:
POST /api/channels/{channelID}/dvr/reverse-sync - HTTP-triggered runs are detached from request cancellation and execute
under an internal timeout budget (
AdminHandler.dvrSyncTimeout, default2m)
- global:
internal/dvr/service.goloads provider custom mapping and lineup stations.- Service maps tuner/channel keys back into channel DVR mapping rows.
- For station entries missing lineup channel, station-ref-only mappings are preserved and warning counts are returned.
- Trigger source:
- channel create/update requests carrying
dynamic_rule POST /api/channelsPATCH /api/channels/{channelID}
- channel create/update requests carrying
internal/http/admin_channels.gonormalizes the channel update response first, then queues background sync work for eligible rules (enabled=trueand non-emptysearch_query).search_queryuses the same token semantics as/api/items?q=...: OR-disjunct separators (|or standaloneOR) with include terms and exclusion tokens prefixed with-or!. Queries with no OR separator preserve legacy include/exclude AND behavior.- token-mode parser limits are runtime-configurable through
catalog-search-max-terms,catalog-search-max-disjuncts, andcatalog-search-max-term-runes(CATALOG_SEARCH_MAX_*env aliases). - truncation remains non-fatal; additive
search_warningresponse metadata reports effective limits and applied/dropped token counts.
- Queue behavior is per-channel and versioned:
- in-flight runs can be superseded by newer updates
- rapid updates are coalesced to the latest request
- disable/delete transitions cancel pending and active sync runs
- execution is detached from HTTP request cancellation; queued/running sync work continues after client disconnect unless canceled by rule-disable/delete transitions or the per-run timeout budget
- Sync execution path:
- list matching active catalog item keys via
catalog.ListActiveItemKeysByCatalogFilter(...) - apply source reconciliation via
channels.Service.SyncDynamicSources(...)
- list matching active catalog item keys via
- Reconciliation behavior:
- adds missing
dynamic_querysources for matched items - removes no-longer-matched
dynamic_querysources - preserves manual source associations
- promotes matching
channel_keyassociations todynamic_query
- adds missing
- Trigger sources:
- playlist reconcile (
internal/reconcile/reconcile.go) - dynamic block CRUD (
/api/dynamic-channels*) through immediate background sync
- playlist reconcile (
channels.Service.SyncDynamicChannelBlocks(...)materializes each enabled query into generated rows inpublished_channelswithchannel_class=dynamic_generated.- Generated rows are allocated in per-block ranges:
block_start = 10000 + (order_index * 1000)- generated rows are capped at
1000per block
- Reorder operations (
PATCH /api/dynamic-channels/{queryID}/channels/reorder) persist deterministic guide-number reassignment within a block. - After successful materialization/reorder changes, admin routes trigger
DVRService.ReloadLineup(...)as a best-effort post-change action so provider-side lineup views can pick up updated10000+guides.
All admin routes are registered in internal/http/admin_routes.go.
Automation routes are conditionally registered when automation dependencies are
wired, and DVR routes are conditionally registered when DVR service is wired.
| Method | Path | Handler | Implementation |
|---|---|---|---|
GET |
/discover.json |
Handler.DiscoverJSON |
internal/hdhr/http_handlers.go |
GET |
/lineup.json |
Handler.LineupJSON |
internal/hdhr/http_handlers.go |
GET |
/lineup.m3u |
Handler.LineupM3U |
internal/hdhr/http_handlers.go |
GET |
/lineup.xml |
Handler.LineupXML |
internal/hdhr/http_handlers.go |
GET |
/lineup_status.json |
Handler.LineupStatusJSON |
internal/hdhr/http_handlers.go |
GET |
/lineup.html |
Handler.LineupHTML |
internal/hdhr/http_handlers.go |
GET |
/upnp/device.xml |
Handler.DeviceDescriptionXML |
internal/hdhr/http_handlers.go |
GET |
/device.xml |
Handler.DeviceDescriptionXML |
internal/hdhr/http_handlers.go |
GET |
/upnp/scpd/connection-manager.xml |
Handler.ConnectionManagerSCPDXML |
internal/hdhr/upnp_control.go |
GET |
/upnp/scpd/content-directory.xml |
Handler.ContentDirectorySCPDXML |
internal/hdhr/upnp_control.go |
POST |
/upnp/control/connection-manager |
Handler.ConnectionManagerControl |
internal/hdhr/upnp_control.go |
POST |
/upnp/control/content-directory |
Handler.ContentDirectoryControl |
internal/hdhr/upnp_control.go |
GET |
/auto/{guide} |
stream.Handler.ServeHTTP |
internal/stream/handler.go |
GET |
/ |
inline redirect (/ui/) |
cmd/hdhriptv/main.go |
GET |
/healthz |
inline handler | cmd/hdhriptv/main.go |
GET |
/metrics |
promhttp handler (optional) | cmd/hdhriptv/main.go |
UDP |
:65001 discovery |
Server.Serve |
internal/hdhr/discovery/udp.go |
UDP |
:1900 SSDP (UPNP_ENABLED=true) |
Server.Serve |
internal/hdhr/upnp/server.go |
Behavior notes:
- Stream routing accepts
/auto/v{guide}and/auto/{guide}; the handler normalizes a leadingvbefore guide-number lookup. - UPnP SSDP responder (when enabled) answers
M-SEARCHforssdp:all,upnp:rootdevice,uuid:<derived DeviceID UDN>,urn:schemas-upnp-org:device:MediaServer:1, andurn:schemas-upnp-org:device:Basic:1,urn:schemas-atsc.org:device:primaryDevice:1.0,urn:schemas-upnp-org:service:ConnectionManager:1, andurn:schemas-upnp-org:service:ContentDirectory:1. - UPnP parser accepts both SSDP
M-SEARCHrequest-line variants used in the wild:HTTP/1.1and legacyHTTP/1.0. - UPnP control surfaces intentionally expose a bounded read-only action subset:
ConnectionManager(GetProtocolInfo,GetCurrentConnectionIDs,GetCurrentConnectionInfo) andContentDirectory(GetSearchCapabilities,GetSortCapabilities,GetSystemUpdateID,Browse). stream.Handler.ServeHTTPapplies the global tune-backoff gate only when a request would create a new shared session/source startup.- If a session for that channel is already active or pending, additional subscribers bypass tune backoff and can join immediately.
- Rejected tune attempts return HTTP
503with aRetry-Afterheader and logreason=global_tune_backoff.
| Method | Path | Handler | Implementation |
|---|---|---|---|
GET |
/ui/ |
handleUIRoot |
internal/http/admin_routes.go |
GET |
/ui/catalog |
handleUICatalog |
internal/http/admin_routes.go |
GET |
/ui/channels |
handleUIChannels |
internal/http/admin_routes.go |
GET |
/ui/channels/{channelID} |
handleUIChannelDetail |
internal/http/admin_routes.go |
GET |
/ui/dynamic-channels/{queryID} |
handleUIDynamicChannelDetail |
internal/http/admin_routes.go |
GET |
/ui/merge |
handleUIMerge |
internal/http/admin_routes.go |
GET |
/ui/tuners |
handleUITuners |
internal/http/admin_routes.go |
GET |
/ui/automation |
handleUIAutomation |
internal/http/admin_routes.go |
GET |
/ui/dvr |
handleUIDVR |
internal/http/admin_routes.go |
UI behavior notes:
/ui/cataloguses a toolbar-driven workflow for high-volume source assignment:- multi-group filter chips backed by
/api/groups - target-channel rapid-add mode (row actions switch to
Add Source) - toolbar-driven dynamic channel creation from current filter context
- multi-group filter chips backed by
/ui/channelsrow metadata highlights dynamic-rule status and per-channel source composition (enabled/total, dynamic-managed count, manual-managed count)./ui/tunersincludes a bottomShared Session Historymaster-detail panel populated from/api/admin/tunerssession_historydata, with status/recovery/error filters, deterministic selection, tabbed detail panes (Summary,Sources,Subscribers,Recovery), and truncation-state messaging.
| Method | Path | Handler | Implementation |
|---|---|---|---|
GET |
/api/groups |
handleGroups |
internal/http/admin_channels.go |
GET |
/api/items |
handleItems |
internal/http/admin_channels.go |
GET |
/api/channels |
handleChannels |
internal/http/admin_channels.go |
POST |
/api/channels |
handleCreateChannel |
internal/http/admin_channels.go |
PATCH |
/api/channels/reorder |
handleReorderChannels |
internal/http/admin_channels.go |
PATCH |
/api/channels/{channelID} |
handleUpdateChannel |
internal/http/admin_channels.go |
DELETE |
/api/channels/{channelID} |
handleDeleteChannel |
internal/http/admin_channels.go |
GET |
/api/dynamic-channels |
handleDynamicChannelQueries |
internal/http/admin_channels.go |
POST |
/api/dynamic-channels |
handleCreateDynamicChannelQuery |
internal/http/admin_channels.go |
GET |
/api/dynamic-channels/{queryID} |
handleGetDynamicChannelQuery |
internal/http/admin_channels.go |
PATCH |
/api/dynamic-channels/{queryID} |
handleUpdateDynamicChannelQuery |
internal/http/admin_channels.go |
DELETE |
/api/dynamic-channels/{queryID} |
handleDeleteDynamicChannelQuery |
internal/http/admin_channels.go |
GET |
/api/dynamic-channels/{queryID}/channels |
handleDynamicGeneratedChannels |
internal/http/admin_channels.go |
PATCH |
/api/dynamic-channels/{queryID}/channels/reorder |
handleReorderDynamicGeneratedChannels |
internal/http/admin_channels.go |
GET |
/api/channels/{channelID}/sources |
handleSources |
internal/http/admin_channels.go |
POST |
/api/channels/{channelID}/sources |
handleAddSource |
internal/http/admin_channels.go |
POST |
/api/channels/{channelID}/sources/health/clear |
handleClearSourceHealth |
internal/http/admin_channels.go |
PATCH |
/api/channels/{channelID}/sources/reorder |
handleReorderSources |
internal/http/admin_channels.go |
PATCH |
/api/channels/{channelID}/sources/{sourceID} |
handleUpdateSource |
internal/http/admin_channels.go |
DELETE |
/api/channels/{channelID}/sources/{sourceID} |
handleDeleteSource |
internal/http/admin_channels.go |
POST |
/api/channels/sources/health/clear |
handleClearAllSourceHealth |
internal/http/admin_channels.go |
GET |
/api/suggestions/duplicates |
handleDuplicateSuggestions |
internal/http/admin_channels.go |
GET |
/api/admin/tuners |
handleTunerStatus |
internal/http/admin_tuners.go |
POST |
/api/admin/tuners/recovery |
handleTriggerTunerRecovery |
internal/http/admin_tuners.go |
Behavior notes:
- Admin mutation handlers that decode JSON through
decodeJSON(...)enforce strict JSON parsing:- unknown fields are rejected (
json.Decoder.DisallowUnknownFields) - trailing data after the first JSON value is rejected
- malformed/invalid JSON returns HTTP
400 - oversized bodies are rejected with HTTP
413viahttp.MaxBytesReader
- unknown fields are rejected (
handleGroups(GET /api/groups) accepts optionalsource_idsfilter (comma-separated or repeated query parameter). When present, returns only groups from the specified playlist sources (deduplicated by name).handleItems(GET /api/items) accepts optionalgroup/group_names,q, andsource_idsfilters pluslimit/offsetpagination hints.- group filter semantics:
- repeated
groupparameters are accepted (?group=News&group=Sports) - comma-separated values are accepted (
?group=News,Sports) group_namesis accepted as a compatibility alias- empty/omitted group filters mean all groups
- repeated
qsupports case-insensitive OR-of-AND include/exclude matching:- include:
fox - exclude:
-spanishor!spanish - disjunct separators:
|or standaloneORkeyword - within each disjunct, terms are AND-combined; across disjuncts, clauses are OR-combined
- exclusion-only queries are allowed
- queries without OR separators preserve legacy include/exclude AND behavior
- include:
- optional
q_regexboolean is accepted (1/0,true/false,yes/no,on/off) and defaults tofalse.q_regex=falsekeeps token/LIKE matching semantics.q_regex=trueevaluatesqas one case-insensitive regex pattern against the full item name string.- regex mode bypasses token operators (
|/OR,-term/!term); those operators apply only in token mode. - invalid regex patterns or overlong regex patterns fail request validation with HTTP
400.
- responses include additive
search_warningmetadata with:mode,truncated- effective limits:
max_terms,max_disjuncts,max_term_runes - counters:
terms_applied,terms_dropped,disjuncts_applied,disjuncts_dropped,term_rune_truncations
- token-mode over-limit inputs return
200and are visibility-reported viasearch_warning.truncated=true(queries are not hard-rejected). limitdefaults to100, clamps to a hard max of1000, and values<1normalize back to100.offsetdefaults to0; negative values normalize to0.- Non-integer
limit/offsetvalues fall back to defaults (they do not return HTTP400).
- group filter semantics:
handleChannels(GET /api/channels) andhandleSources(GET /api/channels/{channelID}/sources) use strict pagination parsing.limitdefaults to200when omitted.- explicit
limit=0is normalized to the same bounded default (200). - hard caps apply (
1000for channels,2000for per-channel sources). - negative or non-integer
limit/offsetvalues return HTTP400.
GET /api/channelsis scoped to traditional channels (channel_class=traditional); generated dynamic rows are surfaced under/api/dynamic-channels/{queryID}/channels.GET /api/channelsresponses include per-channel source summary fields:source_totalsource_enabledsource_dynamic(association_type=dynamic_query)source_manual(association_type!=dynamic_query)
handleClearSourceHealth(POST /api/channels/{channelID}/sources/health/clear) andhandleClearAllSourceHealth(POST /api/channels/sources/health/clear) reset persisted source-health counters/cooldown fields and return{"cleared":<count>}.handleDuplicateSuggestions(GET /api/suggestions/duplicates) normalizes query inputs before delegating to channel duplicate grouping.mindefaults to2, clamps to[2, 100].qis case-insensitive acrosschannel_keyandtvg_id.- legacy
tvg_idquery fallback is accepted whenqis omitted. - response echoes normalized
minandq.
handleTunerStatus(GET /api/admin/tuners) returns live tuner/session rows, per-source virtual tuner summaries (virtual_tunersarray withplaylist_source_id,playlist_source_name,tuner_count,in_use_count,idle_count,active_session_countper source), and bounded shared-session history. Each tuner/session row includesplaylist_source_id,playlist_source_name, andvirtual_tuner_slotfields:- supports optional
resolve_ipboolean query parsing (1/0,true/false,yes/no,on/off; defaultfalse); invalid values return HTTP400. - when
resolve_ip=true, reverse DNS hostnames are added asclient_hoston bothclient_streams[*]andsession_history[*].subscribers[*]when lookup succeeds. session_historyis newest-first and includes active + recently closed sessions tracked in-memory during process lifetime.session_history_limitreports current retention capacity (default256).session_history_truncated_countreports the total number of oldest history entries evicted due to retention.- each
session_historyentry includes per-session timeline guardrails:source_history_limit,source_history_truncated_count,subscriber_history_limit, andsubscriber_history_truncated_count. - history source URLs (
session_history[*].sources[*].stream_url) are sanitized with the same credential/query redaction applied to live status source URLs.
- supports optional
handleTriggerTunerRecovery(POST /api/admin/tuners/recovery) accepts a strict JSON body withchannel_id(required,>0) and optionalreason.- when
reasonis omitted/blank, it defaults toui_manual_trigger. - successful requests return HTTP
200with{"accepted":true,...}. - returns HTTP
503when tuner status/recovery support is not configured. - returns HTTP
404when the channel has no active shared session and HTTP409when a manual recovery request is already pending for that session.
- when
POST /api/channelsandPATCH /api/channels/{channelID}accept optionaldynamic_rulepayloads and return promptly; dynamic source sync runs asynchronously in a background worker managed byadmin_workers.go.dynamic_rulesupports preferredgroup_namesmulti-group payloads and legacygroup_namecompatibility aliasing.dynamic_rule.search_queryfollows the same OR-capable include/exclude semantics asGET /api/itemswhensearch_regex=false(|/ORplus-term/!term; no OR separator keeps legacy AND behavior).dynamic_rule.search_regexis an optional boolean toggle (defaultsfalse) and is persisted with the rule payload.- when enabled, matching evaluates
search_queryas one case-insensitive regex pattern against the full item name string. - token operators apply only when regex mode is disabled.
- invalid regex inputs are rejected before persistence.
- when enabled, matching evaluates
- when both are provided, normalized
group_namessemantics apply andgroup_nameis treated as an alias of the first normalized entry. - create/update responses include additive
search_warningmetadata fordynamic_rule.search_queryusing the same schema as/api/items. - queued/running dynamic sync execution is request-detached and bounded by
AdminHandler.dynamicSyncTimeoutbefore timeout cancellation.
- Dynamic block CRUD handlers queue request-detached immediate block sync work
(
enqueueDynamicBlockSync/runDynamicBlockSyncLoop) with coalescing and cancellation of stale runs; successful changed runs trigger best-effort DVR lineup reload throughDVRService.ReloadLineup(...).- dynamic block create/update/read/list responses include per-query additive
search_warningmetadata so persisted token-mode truncation remains operator-visible in control-plane reads.
- dynamic block create/update/read/list responses include per-query additive
- Regex-mode UI toggles are exposed in the catalog search toolbar, channel
dynamic-rule editor, dynamic block list/create surface, and dynamic block
detail editor so operators can switch between token and regex evaluation
without changing query grammar.
- those UI surfaces also render
search_warningtruncation summaries for token-mode over-limit queries.
- those UI surfaces also render
| Method | Path | Handler | Implementation |
|---|---|---|---|
GET |
/api/admin/playlist-sources |
handleListPlaylistSources |
internal/http/admin_automation.go |
POST |
/api/admin/playlist-sources |
handleCreatePlaylistSource |
internal/http/admin_automation.go |
GET |
/api/admin/playlist-sources/{sourceID} |
handleGetPlaylistSource |
internal/http/admin_automation.go |
PUT |
/api/admin/playlist-sources/{sourceID} |
handleUpdatePlaylistSource |
internal/http/admin_automation.go |
DELETE |
/api/admin/playlist-sources/{sourceID} |
handleDeletePlaylistSource |
internal/http/admin_automation.go |
Behavior notes:
POST /api/admin/playlist-sourcesauto-generates an immutablesource_key(8-byte random hex for newly created sources; legacy shorter keys remain valid). Validates uniquename, uniqueplaylist_url, andtuner_count >= 1.PUT /api/admin/playlist-sources/{sourceID}accepts partial updates forname,playlist_url,tuner_count, andenabled.source_keyis immutable and rejected in write payloads.DELETE /api/admin/playlist-sources/{sourceID}is blocked forsource_id=1(returns HTTP400). Non-primary source deletion removes source-owned catalog rows (no reassignment), removes channel-source mappings for deleted items, removes generated dynamic channels keyed to deleted-source items, and prunes deleted source IDs from dynamic source-filter JSON fields.- Duplicate
nameorplaylist_urlvalues return HTTP400with a descriptive error identifying the conflicting field.
| Method | Path | Handler | Implementation |
|---|---|---|---|
GET |
/api/admin/automation |
handleGetAutomation |
internal/http/admin_automation.go |
PUT |
/api/admin/automation |
handlePutAutomation |
internal/http/admin_automation.go |
POST |
/api/admin/jobs/playlist-sync/run |
handleRunPlaylistSync |
internal/http/admin_automation.go |
POST |
/api/admin/jobs/auto-prioritize/run |
handleRunAutoPrioritize |
internal/http/admin_automation.go |
POST |
/api/admin/jobs/auto-prioritize/cache/clear |
handleClearAutoPrioritizeCache |
internal/http/admin_automation.go |
GET |
/api/admin/jobs/{runID} |
handleGetJobRun |
internal/http/admin_automation.go |
GET |
/api/admin/jobs |
handleListJobRuns |
internal/http/admin_automation.go |
Behavior notes:
handlePutAutomationacquiresAdminHandler.adminConfigMutationMu(defined ininternal/http/admin_routes.go) so automation and DVR config writes are serialized through one mutation critical section.handlePutAutomationapplies partial updates:- top-level keys:
playlist_url,timezone playlist_sourcesarray for bulk source updates (must include all existing sources withsource_id; validates unique names and URLs)- schedule objects:
playlist_sync,auto_prioritize(enabled,cron_spec) - analyzer keys:
probe_timeout_ms,analyzeduration_us,probesize_bytes,bitrate_mode,sample_seconds,enabled_only,top_n_per_channel
- top-level keys:
- Schedule update normalization in
parseScheduleUpdate(...)requirescron_specwhen a schedule resolves toenabled=trueand allows disabling a schedule without validating/storing a cron expression. - Analyzer input validation in
handlePutAutomationenforces:- positive values for
probe_timeout_ms,analyzeduration_us,probesize_bytes, andsample_seconds bitrate_modeallowlist ofmetadata,sample, ormetadata_then_sampletop_n_per_channel >= 0
- positive values for
handlePutAutomationvalidates cron values only when the target schedule is enabled, writes settings, then applies runtime scheduler state viaScheduler.LoadFromSettings(...).- If scheduler apply fails,
handlePutAutomationrestores prior settings via a snapshot/rollback path (snapshotAutomationSettings->restoreAutomationSettings) before returning an error. handleClearAutoPrioritizeCachecallsAutomationSettingsStore.DeleteAllStreamMetrics(...)and returns{"deleted":<count>}with the removed cache row count.startJobRunwrapsRunner.Start(...)withcontext.WithoutCancel(...), so manual trigger request cancellation does not cancel a queued/running job.handleRunPlaylistSyncaccepts optional?source_id=Nquery parameter. When present, validates the source exists and is enabled, and scopes the sync job to refresh only that source. Response includessource_idwhen scoped.handleListJobRunsvalidatesnameagainst the allowlist (playlist_sync,auto_prioritize,dvr_lineup_sync) and normalizes query paging (limitdefault50, clamped1..500;offsetclamped to>= 0).- non-integer
limit/offsetinputs fall back to defaults (50/0). - non-allowlisted
namevalues return HTTP400. - response echoes normalized
name,limit, andoffset.
- non-integer
| Method | Path | Handler | Implementation |
|---|---|---|---|
GET |
/api/admin/dvr |
handleGetDVR |
internal/http/admin_dvr.go |
PUT |
/api/admin/dvr |
handlePutDVR |
internal/http/admin_dvr.go |
POST |
/api/admin/dvr/test |
handleTestDVR |
internal/http/admin_dvr.go |
GET |
/api/admin/dvr/lineups |
handleDVRLineups |
internal/http/admin_dvr.go |
POST |
/api/admin/dvr/sync |
handleDVRSync |
internal/http/admin_dvr.go |
POST |
/api/admin/dvr/reverse-sync |
handleDVRReverseSync |
internal/http/admin_dvr.go |
GET |
/api/channels/dvr |
handleListChannelDVRMappings |
internal/http/admin_dvr.go |
GET |
/api/channels/{channelID}/dvr |
handleGetChannelDVRMapping |
internal/http/admin_dvr.go |
PUT |
/api/channels/{channelID}/dvr |
handlePutChannelDVRMapping |
internal/http/admin_dvr.go |
POST |
/api/channels/{channelID}/dvr/reverse-sync |
handleChannelDVRReverseSync |
internal/http/admin_dvr.go |
Behavior notes:
GET /api/channels/dvrsupports optional query filtering:enabled_only(1,true,yes,on) scopes results to enabled channels.include_dynamic(1,true,yes,on) includes generated dynamic rows.limitandoffsetprovide strict bounded pagination.limitdefaults to200; explicitlimit=0normalizes to200.limitclamps to1000.offsetdefaults to0.- negative or non-integer
limit/offsetvalues return HTTP400.
- default behavior excludes
channel_class=dynamic_generatedrows.
handleListChannelDVRMappingsreturns paged payload metadata:mappings,total,limit,offset,enabled_only, andinclude_dynamic.handleDVRLineups(GET /api/admin/dvr/lineups) accepts optionalrefreshquery parsing with truthy values (1,true,yes,on) and echoes the applied boolean in the response payload asrefresh.handleDVRSync,handleDVRReverseSync, andhandleChannelDVRReverseSyncdecode optional JSON payloads viadecodeOptionalJSON(...).- Empty bodies are accepted, including chunked requests with unknown content length and no payload bytes.
- Non-empty payloads still use strict
decodeJSON(...)parsing, so unknown fields and trailing JSON return HTTP400. - Malformed JSON still returns HTTP
400. - Defaults apply when body content is omitted (for example
dry_run=false,include_dynamic=false).
handlePutDVRalso usesAdminHandler.adminConfigMutationMu, so DVR updates cannot interleave with automation updates.handlePutDVRvalidates enabled sync cron before persisting config, then callsDVRScheduler.UpdateJobSchedule(...)forjobs.JobDVRLineupSync.- If scheduler apply fails after config persistence,
handlePutDVRrestores the prior DVR config (restoreDVRConfig) and returns an error describing whether rollback succeeded. - DVR config response redaction:
jellyfin_api_tokenis write-only and redacted fromGET /api/admin/dvrandPUT /api/admin/dvrresponses.- response payloads expose
jellyfin_api_token_configured=true|false.
- Provider-selection and config fields accepted by
handlePutDVR:provider(primary provider for sync/mapping/test workflows; onlychannelsis accepted)active_providers(post-playlist-sync reload fan-out target set)- per-provider base URLs:
channels_base_urljellyfin_base_url
- legacy
base_url(maps to Channels base URL for compatibility) - optional
jellyfin_api_token(header auth token) - optional
jellyfin_tuner_host_id(host targeting override)