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.
pull/344/head
Broque Thomas 4 weeks ago
parent c3f88a713a
commit 93e036848b

@ -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', ''))

@ -5068,7 +5068,7 @@
<input type="text" id="template-album-path"
placeholder="$albumartist/$albumartist - $album/$track - $title">
<small class="settings-hint">Variables: $albumartist, $artist, $artistletter, $album, $albumtype,
$title, $track, $disc (01), $discnum (1), $year, $quality (filename only)</small>
$title, $track, $disc (01), $discnum (1), $year, $quality (filename only). Use ${var} to append text: ${albumtype}s → Albums</small>
</div>
<div class="form-group">

@ -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}"`);
}
});
}

Loading…
Cancel
Save