Add customizable music video naming and fix slskd log spam
Video naming: new path template in Settings → Paths & Organization with $artist, $artistletter, $title, $year variables. Default unchanged ($artist/$title-video → Artist/Title-video.mp4) so existing Plex setups aren't affected. Users can remove the -video suffix or reorganize however they like. slskd logs: the Clean Search History automation now skips when Soulseek is not the active download source, eliminating noisy connection error logs for users who don't use Soulseek.
This commit is contained in:
parent
a23bce5966
commit
122a6999b3
4 changed files with 52 additions and 10 deletions
|
|
@ -1608,6 +1608,15 @@ def _register_automation_handlers():
|
|||
def _auto_clean_search_history(config):
|
||||
"""Remove old searches from Soulseek."""
|
||||
automation_id = config.get('_automation_id')
|
||||
# Skip if soulseek is not the active download source or in hybrid order
|
||||
dl_mode = config_manager.get('download_source.mode', 'hybrid')
|
||||
hybrid_order = config_manager.get('download_source.hybrid_order', ['hifi', 'youtube', 'soulseek'])
|
||||
soulseek_active = (dl_mode == 'soulseek' or
|
||||
(dl_mode == 'hybrid' and 'soulseek' in hybrid_order))
|
||||
if not soulseek_active or not soulseek_client or not soulseek_client.base_url:
|
||||
_update_automation_progress(automation_id,
|
||||
log_line='Soulseek not active — skipped', log_type='skip')
|
||||
return {'status': 'skipped'}
|
||||
if not config_manager.get('soulseek.auto_clear_searches', True):
|
||||
_update_automation_progress(automation_id,
|
||||
log_line='Auto-clear disabled in settings', log_type='skip')
|
||||
|
|
@ -9360,6 +9369,7 @@ def download_music_video():
|
|||
_music_video_downloads[video_id]['status'] = 'matching'
|
||||
artist_name = raw_channel
|
||||
track_title = raw_title
|
||||
year = ''
|
||||
|
||||
# Strip common YouTube suffixes for cleaner search
|
||||
import re as _re
|
||||
|
|
@ -9384,6 +9394,8 @@ def download_music_video():
|
|||
if best and best_score >= 0.5:
|
||||
artist_name = best.artists[0] if best.artists else raw_channel
|
||||
track_title = best.name
|
||||
if hasattr(best, 'release_date') and best.release_date:
|
||||
year = str(best.release_date)[:4]
|
||||
print(f"[Music Video] Matched to: {artist_name} - {track_title} (confidence: {best_score:.2f})")
|
||||
else:
|
||||
# Parse artist from video title: "Artist - Title" pattern
|
||||
|
|
@ -9403,13 +9415,27 @@ def download_music_video():
|
|||
def _sanitize(s):
|
||||
return _re.sub(r'[<>:"/\\|?*]', '_', s).strip().rstrip('.')
|
||||
|
||||
artist_folder = _sanitize(artist_name)
|
||||
video_filename = f"{_sanitize(track_title)}-video"
|
||||
|
||||
# Build output path: MusicVideos/Artist/Title-video
|
||||
artist_dir = os.path.join(music_videos_path, artist_folder)
|
||||
os.makedirs(artist_dir, exist_ok=True)
|
||||
output_path = os.path.join(artist_dir, video_filename)
|
||||
# Apply video path template
|
||||
video_template = config_manager.get('file_organization.templates', {}).get('video_path', '$artist/$title-video')
|
||||
if not video_template or not video_template.strip():
|
||||
video_template = '$artist/$title-video'
|
||||
safe_artist = _sanitize(artist_name)
|
||||
video_path = video_template
|
||||
video_path = video_path.replace('$artistletter', safe_artist[0].upper() if safe_artist else 'A')
|
||||
video_path = video_path.replace('$artist', safe_artist)
|
||||
video_path = video_path.replace('$title', _sanitize(track_title))
|
||||
video_path = video_path.replace('$year', str(year) if year else '')
|
||||
# Clean up empty segments from missing variables
|
||||
video_path = _re.sub(r'//+', '/', video_path).strip('/')
|
||||
# Split into folder and filename
|
||||
path_parts = video_path.rsplit('/', 1)
|
||||
if len(path_parts) == 2:
|
||||
folder_part, file_part = path_parts
|
||||
else:
|
||||
folder_part, file_part = '', path_parts[0]
|
||||
output_dir = os.path.join(music_videos_path, folder_part) if folder_part else music_videos_path
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_path = os.path.join(output_dir, file_part)
|
||||
|
||||
# Step 2: Download
|
||||
_music_video_downloads[video_id]['status'] = 'downloading'
|
||||
|
|
@ -22503,6 +22529,8 @@ def get_version_info():
|
|||
"• Reject Qobuz 30-second sample/preview downloads",
|
||||
"• Fix library page crash on All filter — non-string soul_id broke card rendering",
|
||||
"• Auto Wing It fallback for failed discovery — unmatched tracks download via Soulseek with raw metadata",
|
||||
"• Customizable music video naming — path template with $artist, $title, $year variables",
|
||||
"• Fix soulseek log spam when not configured as download source",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5085,6 +5085,13 @@
|
|||
<small class="settings-hint">Variables: $playlist, $albumartist, $artist, $artistletter, $title, $year, $quality (filename only)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Music Video Path Template:</label>
|
||||
<input type="text" id="template-video-path"
|
||||
placeholder="$artist/$title-video">
|
||||
<small class="settings-hint">Variables: $artist, $artistletter, $title, $year. Default: $artist/$title-video → Artist/Title-video.mp4</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Multi-Disc Folder Label:</label>
|
||||
<select id="disc-label">
|
||||
|
|
|
|||
|
|
@ -3602,6 +3602,8 @@ const WHATS_NEW = {
|
|||
'2.33': [
|
||||
// --- April 19, 2026 ---
|
||||
{ date: 'April 19, 2026' },
|
||||
{ title: 'Customizable Music Video Naming', desc: 'Music video file naming is now configurable via a path template in Settings → Library → Paths & Organization. Default unchanged (Artist/Title-video.mp4). Remove "-video" from the template to get clean filenames. Available variables: $artist, $artistletter, $title, $year', page: 'settings' },
|
||||
{ title: 'Fix Soulseek Log Spam', desc: 'The "Clean Search History" automation no longer tries to connect to slskd when Soulseek is not the active download source, eliminating noisy connection error logs for users who don\'t use Soulseek' },
|
||||
{ title: 'Auto Wing It Discovery Fallback', desc: 'When playlist discovery fails to match a track on any metadata API (Spotify, Deezer, iTunes, etc.), the track now automatically falls back to Wing It mode instead of being marked "Not Found". Stub metadata is built from the raw source title and artist, and the track flows through the normal download pipeline via Soulseek. Amber "Wing It" badge distinguishes these from API-matched tracks. Works across all discovery sources: YouTube, Tidal, Deezer, Beatport, ListenBrainz, and mirrored playlists. Wing It stubs persist in the DB for mirrored playlists and are re-attempted on future discovery runs so real matches can replace them' },
|
||||
{ title: 'Fix Library Page Crash on All Filter', desc: 'Library page could crash with "No artists found" when viewing all artists if any artist had a non-string soul_id. Individual letter filters worked because the problematic artist wasn\'t in those results. Card rendering is now fault-tolerant — one bad artist card can\'t take down the whole page', page: 'library' },
|
||||
{ title: 'Fix CI Test Failures', desc: 'Fixed test suite failures caused by incomplete dummy config managers missing get_active_media_server() and script.js read encoding on non-UTF-8 locales' },
|
||||
|
|
|
|||
|
|
@ -5652,12 +5652,14 @@ function resetFileOrganizationTemplates() {
|
|||
const defaults = {
|
||||
album: '$albumartist/$albumartist - $album/$track - $title',
|
||||
single: '$artist/$artist - $title/$title',
|
||||
playlist: '$playlist/$artist - $title'
|
||||
playlist: '$playlist/$artist - $title',
|
||||
video: '$artist/$title-video'
|
||||
};
|
||||
|
||||
document.getElementById('template-album-path').value = defaults.album;
|
||||
document.getElementById('template-single-path').value = defaults.single;
|
||||
document.getElementById('template-playlist-path').value = defaults.playlist;
|
||||
document.getElementById('template-video-path').value = defaults.video;
|
||||
|
||||
debouncedAutoSaveSettings();
|
||||
}
|
||||
|
|
@ -5669,7 +5671,8 @@ function validateFileOrganizationTemplates() {
|
|||
const validVars = {
|
||||
album: ['$artist', '$albumartist', '$artistletter', '$album', '$albumtype', '$title', '$track', '$disc', '$discnum', '$year', '$quality'],
|
||||
single: ['$artist', '$albumartist', '$artistletter', '$album', '$albumtype', '$title', '$track', '$year', '$quality'],
|
||||
playlist: ['$artist', '$artistletter', '$playlist', '$title', '$year', '$quality']
|
||||
playlist: ['$artist', '$artistletter', '$playlist', '$title', '$year', '$quality'],
|
||||
video: ['$artist', '$artistletter', '$title', '$year']
|
||||
};
|
||||
|
||||
// Get template values
|
||||
|
|
@ -6191,6 +6194,7 @@ async function loadSettingsData() {
|
|||
document.getElementById('template-album-path').value = settings.file_organization?.templates?.album_path || '$albumartist/$albumartist - $album/$track - $title';
|
||||
document.getElementById('template-single-path').value = settings.file_organization?.templates?.single_path || '$artist/$artist - $title/$title';
|
||||
document.getElementById('template-playlist-path').value = settings.file_organization?.templates?.playlist_path || '$playlist/$artist - $title';
|
||||
document.getElementById('template-video-path').value = settings.file_organization?.templates?.video_path || '$artist/$title-video';
|
||||
document.getElementById('disc-label').value = settings.file_organization?.disc_label || 'Disc';
|
||||
document.getElementById('collab-artist-mode').value = settings.file_organization?.collab_artist_mode || 'first';
|
||||
document.getElementById('allow-duplicate-tracks').checked = settings.wishlist?.allow_duplicate_tracks !== false;
|
||||
|
|
@ -7645,7 +7649,8 @@ async function saveSettings(quiet = false) {
|
|||
templates: {
|
||||
album_path: document.getElementById('template-album-path').value,
|
||||
single_path: document.getElementById('template-single-path').value,
|
||||
playlist_path: document.getElementById('template-playlist-path').value
|
||||
playlist_path: document.getElementById('template-playlist-path').value,
|
||||
video_path: document.getElementById('template-video-path').value
|
||||
}
|
||||
},
|
||||
wishlist: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue