Add $year variable support to file path templates

Introduces the $year variable for album, single, and playlist path templates, allowing users to include the release year in file organization. Updates the backend to extract and provide the year, adjusts the web UI to document the new variable, and updates validation logic to recognize $year as valid in templates.
This commit is contained in:
Broque Thomas 2026-01-02 08:24:43 -08:00
parent 3bd8c4f94c
commit a8935ae8f5
4 changed files with 24 additions and 10 deletions

View file

@ -48,6 +48,7 @@
}, },
"file_organization": { "file_organization": {
"enabled": true, "enabled": true,
"_template_variables": "Available: $artist, $albumartist, $album, $title, $track, $year, $playlist",
"templates": { "templates": {
"album_path": "$albumartist/$albumartist - $album/$track - $title", "album_path": "$albumartist/$albumartist - $album/$track - $title",
"single_path": "$artist/$artist - $title/$title", "single_path": "$artist/$artist - $title/$title",

View file

@ -6948,6 +6948,14 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
original_search = context.get("original_search_result", {}) original_search = context.get("original_search_result", {})
playlist_folder_mode = track_info.get("_playlist_folder_mode", False) playlist_folder_mode = track_info.get("_playlist_folder_mode", False)
# Extract year from spotify_album for template use (safe for all modes)
year = 'Unknown'
spotify_album = context.get("spotify_album", {})
if spotify_album and spotify_album.get('release_date'):
release_date = spotify_album['release_date']
if release_date and len(release_date) >= 4:
year = release_date[:4]
# Determine which template type to use # Determine which template type to use
if playlist_folder_mode: if playlist_folder_mode:
# PLAYLIST MODE # PLAYLIST MODE
@ -6960,7 +6968,8 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
'album': track_name, 'album': track_name,
'title': track_name, 'title': track_name,
'playlist_name': playlist_name, 'playlist_name': playlist_name,
'track_number': 1 'track_number': 1,
'year': year
} }
folder_path, filename_base = _get_file_path_from_template(template_context, 'playlist_path') folder_path, filename_base = _get_file_path_from_template(template_context, 'playlist_path')
@ -6997,7 +7006,8 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
'albumartist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name, 'albumartist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name,
'album': album_info['album_name'], 'album': album_info['album_name'],
'title': clean_track_name, 'title': clean_track_name,
'track_number': track_number 'track_number': track_number,
'year': year
} }
folder_path, filename_base = _get_file_path_from_template(template_context, 'album_path') folder_path, filename_base = _get_file_path_from_template(template_context, 'album_path')
@ -7032,7 +7042,8 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
'albumartist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name, 'albumartist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name,
'album': album_info.get('album_name', clean_track_name) if album_info else clean_track_name, 'album': album_info.get('album_name', clean_track_name) if album_info else clean_track_name,
'title': clean_track_name, 'title': clean_track_name,
'track_number': 1 'track_number': 1,
'year': year
} }
folder_path, filename_base = _get_file_path_from_template(template_context, 'single_path') folder_path, filename_base = _get_file_path_from_template(template_context, 'single_path')
@ -7076,6 +7087,7 @@ def _apply_path_template(template: str, context: dict) -> str:
result = result.replace('$album', context.get('album', 'Unknown Album')) result = result.replace('$album', context.get('album', 'Unknown Album'))
result = result.replace('$title', context.get('title', 'Unknown Track')) result = result.replace('$title', context.get('title', 'Unknown Track'))
result = result.replace('$track', f"{context.get('track_number', 1):02d}") result = result.replace('$track', f"{context.get('track_number', 1):02d}")
result = result.replace('$year', str(context.get('year', 'Unknown')))
return result return result

View file

@ -2772,19 +2772,19 @@
<div class="form-group"> <div class="form-group">
<label>Album Path Template:</label> <label>Album Path Template:</label>
<input type="text" id="template-album-path" placeholder="$albumartist/$albumartist - $album/$track - $title"> <input type="text" id="template-album-path" placeholder="$albumartist/$albumartist - $album/$track - $title">
<small style="color: #888;">Variables: $albumartist, $artist, $album, $title, $track</small> <small style="color: #888;">Variables: $albumartist, $artist, $album, $title, $track, $year</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Single Path Template:</label> <label>Single Path Template:</label>
<input type="text" id="template-single-path" placeholder="$artist/$artist - $title/$title"> <input type="text" id="template-single-path" placeholder="$artist/$artist - $title/$title">
<small style="color: #888;">Variables: $artist, $title, $album</small> <small style="color: #888;">Variables: $artist, $title, $album, $year</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Playlist Path Template:</label> <label>Playlist Path Template:</label>
<input type="text" id="template-playlist-path" placeholder="$playlist/$artist - $title"> <input type="text" id="template-playlist-path" placeholder="$playlist/$artist - $title">
<small style="color: #888;">Variables: $playlist, $artist, $title</small> <small style="color: #888;">Variables: $playlist, $artist, $title, $year</small>
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -1484,9 +1484,9 @@ function validateFileOrganizationTemplates() {
// Valid variables for each template type // Valid variables for each template type
const validVars = { const validVars = {
album: ['$artist', '$albumartist', '$album', '$title', '$track'], album: ['$artist', '$albumartist', '$album', '$title', '$track', '$year'],
single: ['$artist', '$albumartist', '$album', '$title'], single: ['$artist', '$albumartist', '$album', '$title', '$year'],
playlist: ['$artist', '$playlist', '$title'] playlist: ['$artist', '$playlist', '$title', '$year']
}; };
// Get template values // Get template values
@ -7219,7 +7219,8 @@ async function startMissingTracksProcess(playlistId) {
}; };
// If this is an artist album download, use album name and include full context // If this is an artist album download, use album name and include full context
if (playlistId.startsWith('artist_album_')) { // Match both 'artist_album_' and 'enhanced_search_album_' prefixes
if (playlistId.startsWith('artist_album_') || playlistId.startsWith('enhanced_search_album_')) {
requestBody.playlist_name = process.album?.name || process.playlist.name; requestBody.playlist_name = process.album?.name || process.playlist.name;
requestBody.is_album_download = true; requestBody.is_album_download = true;
requestBody.album_context = process.album; // Full Spotify album object requestBody.album_context = process.album; // Full Spotify album object