Files

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:

  1. Custom variable passresolveVariables(content, effectiveVars) replaces {custom_token} placeholders. effectiveVars is 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.
  2. Video token passresolveVideoVars() replaces {video.*} tokens using the video's own fields.
  3. Collaborator token passresolveCollaborators() or expandCollaboratorBlock() replaces {collab.*} tokens.

Effective priority (highest → lowest):

  1. System tokens ({video.*}, {collab.*}) — always win; cannot be shadowed by any variable name
  2. Video-level variables (VideoConfig.variableValues) — override team variables on conflict
  3. Team variables (TeamVariable table) — 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.

  • 02 - Backend
  • 02 - Description Engine (features)