7.7 KiB
Render Engine
Two Services
VideoRenderService
Location: backend/src/shared/render-engine/video-render.service.ts
The single source of truth for description rendering. Responsible for fetching all required data:
- Description blocks (from
blockOrder) - Block overrides and variable values (from
VideoConfig) - Team variables
- Campaign blocks (active ones are appended regardless of
blockOrder) - Team date format and timezone
- Playlists (for
{video.playlists}token) - Collaborators (for
{collab.*}tokens)
Then delegates computation to RenderEngineService.
Used by: youtube-sync.processor, render.processor, videos.service.ts renderDescription()
RenderEngineService
Location: backend/src/shared/render-engine/render-engine.service.ts
Pure computation. Takes RenderInput, returns { rendered: string, hash: string }.
Used directly by: video-configs.service.ts renderPreview(), templates.service.ts renderPreview() — these callers already have all data fetched.
Block Rendering by Type
| Block Type | Rendering Behavior |
|---|---|
STATIC |
Raw content output, zero token substitution |
VARIABLE |
Resolves {custom_var}, {video.*}, {collab.*} tokens |
CAMPAIGN |
Auto-appended at end when campaign.startAt ≤ now ≤ campaign.endAt |
COLLABORATOR |
Cloned once per assigned collaborator, joined with \n\n (or \n if compact) |
CONDITIONAL |
Evaluates condition JSON against video data |
GLOBAL, REPEATABLE |
Deprecated; render as VARIABLE |
Freetext Entries
blockOrder may contain IDs prefixed freetext: (e.g. freetext:abc123). These have no corresponding DescriptionBlock row. Their content comes entirely from blockOverrides[id].content. The compact flag for freetext entries also comes from blockOverrides[id].compact.
Token Resolution
Resolution happens in three distinct passes per block, applied sequentially:
- Custom variable pass —
resolveVariables(content, effectiveVars)replaces{custom_token}placeholders.effectiveVarsis built by merging team variables and video-level values:{ ...teamVariables, ...variableValues }. Video-level values win on any name collision — last-write-wins from the spread. System tokens (video.*,collab.*) are explicitly skipped here. - Video token pass —
resolveVideoVars()replaces{video.*}tokens using the video's own fields. - Collaborator token pass —
resolveCollaborators()orexpandCollaboratorBlock()replaces{collab.*}tokens.
Effective priority (highest → lowest):
- System tokens (
{video.*},{collab.*}) — always win; cannot be shadowed by any variable name - Video-level variables (
VideoConfig.variableValues) — override team variables on conflict - Team variables (
TeamVariabletable) — baseline for custom tokens
Because system tokens are skipped in pass 1 and resolved in dedicated passes afterwards, naming a team or video variable video.title has no effect — it is silently ignored and the actual video title is substituted instead.
DescriptionBlock.variableDefinitions declares which tokens a block expects but is never read by the render engine — it is metadata only (see Backlog).
Any token in SYSTEM_VARIABLE_TOKENS set is skipped by resolveVariables() and handled by its dedicated resolver. If you add a new {video.*} or {collab.*} token, it must be registered in the system variables registry.
System Variable Tokens
Video tokens:
| Token | Resolves to |
|---|---|
{video.title} |
Video title |
{video.tags} |
Comma-separated tag list |
{video.category} |
YouTube category name (e.g. Gaming). Falls back to the raw numeric ID if the category is not in the known map. There is no {video.categoryId} token. |
{video.scheduledAt} |
Scheduled publish date (date token — supports |format) |
{video.recordingDate} |
Recording date (date token — supports |format) |
{video.gameTitle} |
Game title field |
{video.language} |
Default language code |
{video.playlists} |
Comma-separated list of playlist titles (e.g. Gaming, Tutorials) |
{video.playlistLinks} |
Comma-separated list of full YouTube playlist URLs (e.g. https://www.youtube.com/playlist?list=PLxxx) |
Collaborator tokens:
{collab.name}, {collab.youtube}, {collab.twitch}, {collab.instagram}, {collab.tiktok}, {collab.twitter}, {collab.bluesky}, {collab.discord}, {collab.aliases}, {collab.notes}
Note: {collab.youtube} resolves to the full URL (https://www.youtube.com/@handle), not just the handle.
Multi-Collaborator Rendering
Two different behaviors apply depending on block type:
In VARIABLE, CONDITIONAL, CAMPAIGN, and freetext blocks
{collab.*} tokens resolve against the first collaborator in Video.collaboratorIds whose record is found in the loaded collaborator list. If no collaborator is assigned, all {collab.*} tokens are left unreplaced.
This means if a video has three collaborators, {collab.name} in a VARIABLE block only produces the first collaborator's name — not all three.
In COLLABORATOR blocks
The block's content template is expanded once per assigned collaborator, in the order they appear in collaboratorIds. Each expansion resolves all {collab.*} tokens against that specific collaborator. The resulting strings are joined with \n\n (double newline), or \n if the block has compact: true.
Example — with two collaborators (Alice, Bob) and a COLLABORATOR block containing:
🎮 {collab.name} — {collab.youtube}
Renders as:
🎮 Alice — https://www.youtube.com/@alice
🎮 Bob — https://www.youtube.com/@bob
If a collaborator has no value for a given token (e.g. no youtubeLink), that token resolves to an empty string.
Hash Computation
hashMetadata() in shared/render-engine/hash.ts hashes these fields: title, description, tags, categoryId, privacyStatus, defaultLanguage, defaultAudioLanguage, selfDeclaredMadeForKids, embeddable, license, recordingDate.
The hash is stored as Video.lastSyncedHash after a successful YouTube push. hasPendingChanges is computed by comparing current state hash against lastSyncedHash.
Date Format Override Syntax
Date tokens support an inline format override using | as a separator:
{video.scheduledAt|DD.MM.YYYY}
{video.recordingDate|MMMM D, YYYY}
Only date tokens support this. The two date tokens are video.scheduledAt and video.recordingDate. All other video.* and collab.* tokens are non-date (kind: 'simple') — the regex captures the |format portion for all tokens but non-date resolvers ignore it.
Format string tokens (custom implementation, not strftime or moment.js):
| Token | Output |
|---|---|
YYYY |
4-digit year (e.g. 2024) |
YY |
2-digit year (e.g. 24) |
MMMM |
Full month name (e.g. January) |
MMM |
Short month name (e.g. Jan) |
MM |
Zero-padded month (e.g. 01) |
M |
Month without padding (e.g. 1) |
DD |
Zero-padded day (e.g. 05) |
D |
Day without padding (e.g. 5) |
Priority: inline token format → team dateFormat setting → YYYY-MM-DD (hard default).
Invalid format strings are silently passed through as-is. Any character or sequence not matching a known token is output verbatim — no error, no warning.
Null date: If the date field is null or undefined (e.g. video.recordingDate not set), the token resolves to an empty string regardless of format.
Date Formatting Implementation
applyDateFormat() in render-engine.service.ts uses a single-pass regex replacement to avoid re-substitution bugs. Do not convert to chained .replace() calls.
Related
- 02 - Backend
- 02 - Description Engine (features)