From 93e036848b6a3e41acbdf25166e7972018e45d0b Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:03:11 -0700 Subject: [PATCH] Add ${var} delimiter syntax for path templates Users can now append literal text to template variables using curly braces: ${albumtype}s produces "Albums", "Singles", "EPs". Without braces, $albumtypes was rejected as an unknown variable by validation. Both syntaxes work: $albumtype (plain) and ${albumtype} (delimited). Bracket vars are resolved first to prevent partial matching conflicts. Validation updated for album, single, and playlist templates. --- web_server.py | 19 +++++++++++++++++++ webui/index.html | 2 +- webui/static/script.js | 34 +++++++++++++++++++--------------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/web_server.py b/web_server.py index d888fda3..12665afb 100644 --- a/web_server.py +++ b/web_server.py @@ -18498,6 +18498,25 @@ def _apply_path_template(template: str, context: dict) -> str: album_artist_value = resolved except Exception: pass + # Support ${var} delimited syntax (e.g. ${albumtype}s → Albums) + # Must run before $var replacements to prevent partial matching + _bracket_map = { + 'albumartist': album_artist_value, + 'albumtype': clean_context.get('albumtype', 'Album'), + 'playlist': clean_context.get('playlist_name', ''), + 'artistletter': (clean_context.get('artist', 'U') or 'U')[0].upper(), + 'artist': clean_context.get('artist', 'Unknown Artist'), + 'album': clean_context.get('album', 'Unknown Album'), + 'title': clean_context.get('title', 'Unknown Track'), + 'track': f"{clean_context.get('track_number', 1):02d}", + 'disc': str(clean_context.get('disc_number', 1)), + 'discnum': str(clean_context.get('disc_number', 1)), + 'year': str(clean_context.get('year', '')), + 'quality': clean_context.get('quality', ''), + } + for var_name, val in _bracket_map.items(): + result = result.replace('${' + var_name + '}', val) + result = result.replace('$albumartist', album_artist_value) result = result.replace('$albumtype', clean_context.get('albumtype', 'Album')) result = result.replace('$playlist', clean_context.get('playlist_name', '')) diff --git a/webui/index.html b/webui/index.html index f1c63959..fcca3368 100644 --- a/webui/index.html +++ b/webui/index.html @@ -5068,7 +5068,7 @@ Variables: $albumartist, $artist, $artistletter, $album, $albumtype, - $title, $track, $disc (01), $discnum (1), $year, $quality (filename only) + $title, $track, $disc (01), $discnum (1), $year, $quality (filename only). Use ${var} to append text: ${albumtype}s → Albums
diff --git a/webui/static/script.js b/webui/static/script.js index 8b7cdc96..e9a3aacc 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -5703,17 +5703,19 @@ function validateFileOrganizationTemplates() { errors.push('Album template cannot have consecutive slashes //'); } // Check for likely typos of valid variables (case-insensitive to catch $Album, $ARTIST, etc.) - const albumVarPattern = /\$[a-zA-Z]+/g; + const albumVarPattern = /\$\{([a-zA-Z]+)\}|\$([a-zA-Z]+)/g; const foundVars = albumPath.match(albumVarPattern) || []; foundVars.forEach(v => { - const lowerVar = v.toLowerCase(); + // Normalize ${var} to $var for validation + const normalized = v.startsWith('${') ? '$' + v.slice(2, -1) : v; + const lowerVar = normalized.toLowerCase(); // Check if lowercase version exists in valid vars const isValid = validVars.album.some(validVar => validVar.toLowerCase() === lowerVar); if (!isValid) { - errors.push(`Invalid variable "${v}" in album template. Valid: ${validVars.album.join(', ')}`); - } else if (v !== lowerVar && validVars.album.includes(lowerVar)) { + errors.push(`Invalid variable "${normalized}" in album template. Valid: ${validVars.album.join(', ')}`); + } else if (normalized !== lowerVar && validVars.album.includes(lowerVar)) { // Variable is valid but has wrong case - errors.push(`Variable "${v}" should be lowercase: "${lowerVar}"`); + errors.push(`Variable "${normalized}" should be lowercase: "${lowerVar}"`); } }); } @@ -5730,15 +5732,16 @@ function validateFileOrganizationTemplates() { if (singlePath.includes('//')) { errors.push('Single template cannot have consecutive slashes //'); } - const singleVarPattern = /\$[a-zA-Z]+/g; + const singleVarPattern = /\$\{([a-zA-Z]+)\}|\$([a-zA-Z]+)/g; const foundVars = singlePath.match(singleVarPattern) || []; foundVars.forEach(v => { - const lowerVar = v.toLowerCase(); + const normalized = v.startsWith('${') ? '$' + v.slice(2, -1) : v; + const lowerVar = normalized.toLowerCase(); const isValid = validVars.single.some(validVar => validVar.toLowerCase() === lowerVar); if (!isValid) { - errors.push(`Invalid variable "${v}" in single template. Valid: ${validVars.single.join(', ')}`); - } else if (v !== lowerVar && validVars.single.includes(lowerVar)) { - errors.push(`Variable "${v}" should be lowercase: "${lowerVar}"`); + errors.push(`Invalid variable "${normalized}" in single template. Valid: ${validVars.single.join(', ')}`); + } else if (normalized !== lowerVar && validVars.single.includes(lowerVar)) { + errors.push(`Variable "${normalized}" should be lowercase: "${lowerVar}"`); } }); } @@ -5757,15 +5760,16 @@ function validateFileOrganizationTemplates() { if (playlistPath.includes('//')) { errors.push('Playlist template cannot have consecutive slashes //'); } - const playlistVarPattern = /\$[a-zA-Z]+/g; + const playlistVarPattern = /\$\{([a-zA-Z]+)\}|\$([a-zA-Z]+)/g; const foundVars = playlistPath.match(playlistVarPattern) || []; foundVars.forEach(v => { - const lowerVar = v.toLowerCase(); + const normalized = v.startsWith('${') ? '$' + v.slice(2, -1) : v; + const lowerVar = normalized.toLowerCase(); const isValid = validVars.playlist.some(validVar => validVar.toLowerCase() === lowerVar); if (!isValid) { - errors.push(`Invalid variable "${v}" in playlist template. Valid: ${validVars.playlist.join(', ')}`); - } else if (v !== lowerVar && validVars.playlist.includes(lowerVar)) { - errors.push(`Variable "${v}" should be lowercase: "${lowerVar}"`); + errors.push(`Invalid variable "${normalized}" in playlist template. Valid: ${validVars.playlist.join(', ')}`); + } else if (normalized !== lowerVar && validVars.playlist.includes(lowerVar)) { + errors.push(`Variable "${normalized}" should be lowercase: "${lowerVar}"`); } }); }