|
|
|
|
@ -16870,76 +16870,6 @@ def _normalize_base_album_name(base_album: str, artist_name: str) -> str:
|
|
|
|
|
logger.info(f"Album variant normalization: '{base_album}' -> '{normalized}'")
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: str = None) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Smart album grouping: Start with standard, upgrade to deluxe if ANY track is deluxe.
|
|
|
|
|
This ensures all tracks from the same album get the same folder name.
|
|
|
|
|
(Adapted from GUI downloads.py)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
with album_cache_lock:
|
|
|
|
|
artist_name = spotify_artist["name"]
|
|
|
|
|
detected_album = album_info.get('album_name', '')
|
|
|
|
|
|
|
|
|
|
# Extract base album name (without edition indicators)
|
|
|
|
|
if detected_album:
|
|
|
|
|
base_album = _get_base_album_name(detected_album)
|
|
|
|
|
elif original_album:
|
|
|
|
|
# Clean the original Soulseek album name
|
|
|
|
|
cleaned_original = _clean_album_title_web(original_album, artist_name)
|
|
|
|
|
base_album = _get_base_album_name(cleaned_original)
|
|
|
|
|
else:
|
|
|
|
|
base_album = _get_base_album_name(detected_album)
|
|
|
|
|
|
|
|
|
|
# Normalize the base name (handle case variations, etc.)
|
|
|
|
|
base_album = _normalize_base_album_name(base_album, artist_name)
|
|
|
|
|
|
|
|
|
|
# Create a key for this album group (artist + base album)
|
|
|
|
|
album_key = f"{artist_name}::{base_album}"
|
|
|
|
|
|
|
|
|
|
# Check if we already have a cached result for this album
|
|
|
|
|
if album_key in album_name_cache:
|
|
|
|
|
cached_name = album_name_cache[album_key]
|
|
|
|
|
logger.info(f"Using cached album name for '{album_key}': '{cached_name}'")
|
|
|
|
|
return cached_name
|
|
|
|
|
|
|
|
|
|
logger.info(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
|
|
|
|
|
|
|
|
|
|
# Check if this track indicates a deluxe edition
|
|
|
|
|
is_deluxe_track = False
|
|
|
|
|
if detected_album:
|
|
|
|
|
is_deluxe_track = _detect_deluxe_edition(detected_album)
|
|
|
|
|
elif original_album:
|
|
|
|
|
is_deluxe_track = _detect_deluxe_edition(original_album)
|
|
|
|
|
|
|
|
|
|
# Get current edition level for this album group (default to standard)
|
|
|
|
|
current_edition = album_editions.get(album_key, "standard")
|
|
|
|
|
|
|
|
|
|
# SMART ALGORITHM: Upgrade to deluxe if this track is deluxe
|
|
|
|
|
if is_deluxe_track and current_edition == "standard":
|
|
|
|
|
logger.info(f"UPGRADE: Album '{base_album}' upgraded from standard to deluxe!")
|
|
|
|
|
album_editions[album_key] = "deluxe"
|
|
|
|
|
current_edition = "deluxe"
|
|
|
|
|
|
|
|
|
|
# Build final album name based on edition level
|
|
|
|
|
if current_edition == "deluxe":
|
|
|
|
|
final_album_name = f"{base_album} (Deluxe Edition)"
|
|
|
|
|
else:
|
|
|
|
|
final_album_name = base_album
|
|
|
|
|
|
|
|
|
|
# Store the resolution in both caches
|
|
|
|
|
album_groups[album_key] = final_album_name
|
|
|
|
|
album_name_cache[album_key] = final_album_name
|
|
|
|
|
album_artists[album_key] = artist_name
|
|
|
|
|
|
|
|
|
|
logger.info(f"Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})")
|
|
|
|
|
|
|
|
|
|
return final_album_name
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error resolving album group: {e}")
|
|
|
|
|
return album_info.get('album_name', 'Unknown Album')
|
|
|
|
|
|
|
|
|
|
def _clean_album_title_web(album_title: str, artist_name: str) -> str:
|
|
|
|
|
"""Clean up album title by removing common prefixes, suffixes, and artist redundancy"""
|
|
|
|
|
import re
|
|
|
|
|
@ -17451,302 +17381,6 @@ def _compute_m3u_folder(transfer_dir, context_type, playlist_name, artist_name='
|
|
|
|
|
return os.path.join(transfer_dir, playlist_sanitized)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
|
|
|
|
|
"""
|
|
|
|
|
SHARED PATH BUILDER - Used by both post-processing AND verification.
|
|
|
|
|
This ensures they always produce the same path.
|
|
|
|
|
|
|
|
|
|
Returns: (final_path, folder_created_successfully)
|
|
|
|
|
"""
|
|
|
|
|
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
|
|
|
|
|
track_info = context.get("track_info", {})
|
|
|
|
|
original_search = context.get("original_search_result", {})
|
|
|
|
|
playlist_folder_mode = track_info.get("_playlist_folder_mode", False)
|
|
|
|
|
|
|
|
|
|
# ENHANCE BYPASS: Place file in same location as original (different extension OK)
|
|
|
|
|
_source_info = track_info.get('source_info') or {}
|
|
|
|
|
if isinstance(_source_info, str):
|
|
|
|
|
try:
|
|
|
|
|
_source_info = json.loads(_source_info)
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
_source_info = {}
|
|
|
|
|
if _source_info.get('enhance') and _source_info.get('original_file_path'):
|
|
|
|
|
original_path = _source_info['original_file_path']
|
|
|
|
|
original_dir = os.path.dirname(original_path)
|
|
|
|
|
original_stem = os.path.splitext(os.path.basename(original_path))[0]
|
|
|
|
|
final_path = os.path.join(original_dir, original_stem + file_ext)
|
|
|
|
|
os.makedirs(original_dir, exist_ok=True)
|
|
|
|
|
logger.info(f"[Enhance] Using original file location: {final_path}")
|
|
|
|
|
return final_path, True
|
|
|
|
|
|
|
|
|
|
# Extract year and album_type from spotify_album for template use (safe for all modes)
|
|
|
|
|
year = '' # Empty string instead of 'Unknown' to avoid "Unknown albumName"
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
# Album type for $albumtype template variable (Album, EP, Single, Compilation)
|
|
|
|
|
raw_album_type = ''
|
|
|
|
|
if spotify_album:
|
|
|
|
|
raw_album_type = spotify_album.get('album_type', '') or ''
|
|
|
|
|
total_tracks = (spotify_album.get('total_tracks', 0) or 0) if spotify_album else 0
|
|
|
|
|
|
|
|
|
|
if raw_album_type.lower() == 'compilation':
|
|
|
|
|
album_type_display = 'Compilation'
|
|
|
|
|
elif raw_album_type.lower() == 'album':
|
|
|
|
|
album_type_display = 'Album'
|
|
|
|
|
elif raw_album_type.lower() in ('single', 'ep'):
|
|
|
|
|
# Spotify labels both singles and EPs as 'single' — use track count to distinguish
|
|
|
|
|
if total_tracks <= 3:
|
|
|
|
|
album_type_display = 'Single'
|
|
|
|
|
elif total_tracks <= 6:
|
|
|
|
|
album_type_display = 'EP'
|
|
|
|
|
else:
|
|
|
|
|
album_type_display = 'Album'
|
|
|
|
|
elif not raw_album_type:
|
|
|
|
|
# No album_type from API (e.g. iTunes source) — infer from track count
|
|
|
|
|
if total_tracks <= 0:
|
|
|
|
|
album_type_display = 'Album' # Unknown, safe default
|
|
|
|
|
elif total_tracks <= 3:
|
|
|
|
|
album_type_display = 'Single'
|
|
|
|
|
elif total_tracks <= 6:
|
|
|
|
|
album_type_display = 'EP'
|
|
|
|
|
else:
|
|
|
|
|
album_type_display = 'Album'
|
|
|
|
|
else:
|
|
|
|
|
album_type_display = raw_album_type.capitalize() or 'Album'
|
|
|
|
|
|
|
|
|
|
# Determine which template type to use
|
|
|
|
|
if playlist_folder_mode:
|
|
|
|
|
# PLAYLIST MODE
|
|
|
|
|
playlist_name = track_info.get("_playlist_name", "Unknown Playlist")
|
|
|
|
|
track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track')
|
|
|
|
|
|
|
|
|
|
_artists = original_search.get('artists') or track_info.get('artists') or []
|
|
|
|
|
|
|
|
|
|
template_context = {
|
|
|
|
|
'artist': 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': track_name,
|
|
|
|
|
'title': track_name,
|
|
|
|
|
'playlist_name': playlist_name,
|
|
|
|
|
'track_number': 1,
|
|
|
|
|
'disc_number': 1,
|
|
|
|
|
'year': year,
|
|
|
|
|
'quality': context.get('_audio_quality', ''),
|
|
|
|
|
'albumtype': album_type_display,
|
|
|
|
|
'_artists_list': _artists,
|
|
|
|
|
'_itunes_artist_id': str(spotify_artist.get('id', '')) if isinstance(spotify_artist, dict) and str(spotify_artist.get('id', '')).isdigit() and (original_search.get('_source') == 'itunes' or track_info.get('_source') == 'itunes') else None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
folder_path, filename_base = _get_file_path_from_template(template_context, 'playlist_path')
|
|
|
|
|
if folder_path and filename_base:
|
|
|
|
|
final_path = os.path.join(transfer_dir, folder_path, filename_base + file_ext)
|
|
|
|
|
os.makedirs(os.path.join(transfer_dir, folder_path), exist_ok=True)
|
|
|
|
|
return final_path, True
|
|
|
|
|
else:
|
|
|
|
|
# Fallback
|
|
|
|
|
playlist_name_sanitized = _sanitize_filename(playlist_name)
|
|
|
|
|
playlist_dir = os.path.join(transfer_dir, playlist_name_sanitized)
|
|
|
|
|
os.makedirs(playlist_dir, exist_ok=True)
|
|
|
|
|
artist_name_sanitized = _sanitize_filename(template_context['artist'])
|
|
|
|
|
track_name_sanitized = _sanitize_filename(track_name)
|
|
|
|
|
new_filename = f"{artist_name_sanitized} - {track_name_sanitized}{file_ext}"
|
|
|
|
|
return os.path.join(playlist_dir, new_filename), True
|
|
|
|
|
|
|
|
|
|
elif album_info and album_info.get('is_album'):
|
|
|
|
|
# ALBUM MODE
|
|
|
|
|
clean_track_name = album_info.get('clean_track_name', 'Unknown Track')
|
|
|
|
|
if original_search.get('spotify_clean_title'):
|
|
|
|
|
clean_track_name = original_search['spotify_clean_title']
|
|
|
|
|
elif album_info.get('clean_track_name'):
|
|
|
|
|
clean_track_name = album_info['clean_track_name']
|
|
|
|
|
else:
|
|
|
|
|
clean_track_name = original_search.get('title', 'Unknown Track')
|
|
|
|
|
|
|
|
|
|
track_number = album_info.get('track_number', 1)
|
|
|
|
|
if track_number is None or not isinstance(track_number, int) or track_number < 1:
|
|
|
|
|
track_number = 1
|
|
|
|
|
|
|
|
|
|
# Multi-disc album subfolder support
|
|
|
|
|
disc_number = album_info.get('disc_number', 1)
|
|
|
|
|
|
|
|
|
|
# Get structured artists list for collab artist handling
|
|
|
|
|
_artists = original_search.get('artists') or track_info.get('artists') or []
|
|
|
|
|
# Extract iTunes artist ID for primary artist resolution (only for iTunes source)
|
|
|
|
|
_spotify_album = context.get('spotify_album', {})
|
|
|
|
|
_itunes_aid = None
|
|
|
|
|
_track_source = original_search.get('_source') or track_info.get('_source', '')
|
|
|
|
|
_is_itunes = _track_source == 'itunes' or (isinstance(spotify_artist, dict) and str(spotify_artist.get('id', '')).isdigit() and _track_source != 'deezer')
|
|
|
|
|
if _is_itunes and isinstance(spotify_artist, dict):
|
|
|
|
|
_aid = spotify_artist.get('id', '')
|
|
|
|
|
if str(_aid).isdigit():
|
|
|
|
|
_itunes_aid = str(_aid)
|
|
|
|
|
if not _itunes_aid and _spotify_album:
|
|
|
|
|
_ext = _spotify_album.get('external_urls', {})
|
|
|
|
|
if isinstance(_ext, dict) and _ext.get('itunes_artist_id'):
|
|
|
|
|
_itunes_aid = _ext['itunes_artist_id']
|
|
|
|
|
|
|
|
|
|
# Resolve album-level artist name for $albumartist.
|
|
|
|
|
# Per-track spotify_artist may vary on collab albums or after artist name changes.
|
|
|
|
|
# Prefer stable album-level sources so all tracks land in the same folder.
|
|
|
|
|
_artist_name = spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name
|
|
|
|
|
_album_artist_name = _artist_name # default: same as track artist
|
|
|
|
|
|
|
|
|
|
# Build album-level artists list for collab mode resolution.
|
|
|
|
|
# Using album-level artists (instead of per-track _artists) ensures collab mode
|
|
|
|
|
# produces the SAME result for every track, preventing folder/tag splits.
|
|
|
|
|
_album_artists_for_collab = None # None = fall back to per-track _artists
|
|
|
|
|
_explicit_artist_ctx = track_info.get('_explicit_artist_context') if isinstance(track_info, dict) else None
|
|
|
|
|
if isinstance(_explicit_artist_ctx, dict) and _explicit_artist_ctx.get('name'):
|
|
|
|
|
_album_artist_name = _explicit_artist_ctx['name']
|
|
|
|
|
_album_artists_for_collab = [_explicit_artist_ctx]
|
|
|
|
|
elif isinstance(_explicit_artist_ctx, str) and _explicit_artist_ctx:
|
|
|
|
|
_album_artist_name = _explicit_artist_ctx
|
|
|
|
|
_album_artists_for_collab = [{'name': _explicit_artist_ctx}]
|
|
|
|
|
else:
|
|
|
|
|
_sa_artists = _spotify_album.get('artists', []) if _spotify_album else []
|
|
|
|
|
if _sa_artists:
|
|
|
|
|
_first_sa = _sa_artists[0]
|
|
|
|
|
if isinstance(_first_sa, dict) and _first_sa.get('name'):
|
|
|
|
|
_album_artist_name = _first_sa['name']
|
|
|
|
|
elif isinstance(_first_sa, str) and _first_sa:
|
|
|
|
|
_album_artist_name = _first_sa
|
|
|
|
|
_album_artists_for_collab = _sa_artists
|
|
|
|
|
|
|
|
|
|
template_context = {
|
|
|
|
|
'artist': _artist_name,
|
|
|
|
|
'albumartist': _album_artist_name,
|
|
|
|
|
'album': album_info['album_name'],
|
|
|
|
|
'title': clean_track_name,
|
|
|
|
|
'track_number': track_number,
|
|
|
|
|
'disc_number': disc_number,
|
|
|
|
|
'year': year,
|
|
|
|
|
'quality': context.get('_audio_quality', ''),
|
|
|
|
|
'albumtype': album_type_display,
|
|
|
|
|
'_artists_list': _album_artists_for_collab if _album_artists_for_collab else _artists,
|
|
|
|
|
'_itunes_artist_id': _itunes_aid,
|
|
|
|
|
}
|
|
|
|
|
spotify_album = context.get('spotify_album', {})
|
|
|
|
|
total_discs = spotify_album.get('total_discs', 1) if spotify_album else 1
|
|
|
|
|
|
|
|
|
|
# Single-track downloads from search don't know total_discs — resolve it
|
|
|
|
|
if total_discs <= 1 and spotify_album and spotify_album.get('id'):
|
|
|
|
|
# Quick check: if this track is on disc 2+, album is definitely multi-disc
|
|
|
|
|
if disc_number > 1:
|
|
|
|
|
total_discs = disc_number
|
|
|
|
|
else:
|
|
|
|
|
# Fetch album tracks to compute total_discs (cached by metadata client)
|
|
|
|
|
try:
|
|
|
|
|
_album_id = spotify_album['id']
|
|
|
|
|
_fb_client = _get_metadata_fallback_client()
|
|
|
|
|
if _fb_client:
|
|
|
|
|
_atd = _fb_client.get_album_tracks(str(_album_id))
|
|
|
|
|
if _atd and _atd.get('items'):
|
|
|
|
|
total_discs = max((t.get('disc_number', 1) for t in _atd['items']), default=1)
|
|
|
|
|
if total_discs > 1:
|
|
|
|
|
spotify_album['total_discs'] = total_discs
|
|
|
|
|
logger.info(f"[Multi-Disc] Resolved {total_discs} discs for single-track download of '{spotify_album.get('name')}'")
|
|
|
|
|
except Exception as _disc_err:
|
|
|
|
|
logger.warning(f"[Multi-Disc] Could not resolve total_discs: {_disc_err}")
|
|
|
|
|
|
|
|
|
|
# Check if user controls disc structure via $disc in their template
|
|
|
|
|
album_template = config_manager.get('file_organization.templates.album_path', '')
|
|
|
|
|
user_controls_disc = '$disc' in album_template
|
|
|
|
|
|
|
|
|
|
disc_label = config_manager.get('file_organization.disc_label', 'Disc')
|
|
|
|
|
|
|
|
|
|
# total_discs was resolved above — pass it into the template context so
|
|
|
|
|
# $cdnum can emit "CDxx" only for real multi-disc albums.
|
|
|
|
|
template_context['total_discs'] = total_discs
|
|
|
|
|
|
|
|
|
|
folder_path, filename_base = _get_file_path_from_template(template_context, 'album_path')
|
|
|
|
|
if folder_path and filename_base:
|
|
|
|
|
if total_discs > 1 and not user_controls_disc:
|
|
|
|
|
disc_folder = f"{disc_label} {disc_number}"
|
|
|
|
|
final_path = os.path.join(transfer_dir, folder_path, disc_folder, filename_base + file_ext)
|
|
|
|
|
os.makedirs(os.path.join(transfer_dir, folder_path, disc_folder), exist_ok=True)
|
|
|
|
|
else:
|
|
|
|
|
final_path = os.path.join(transfer_dir, folder_path, filename_base + file_ext)
|
|
|
|
|
os.makedirs(os.path.join(transfer_dir, folder_path), exist_ok=True)
|
|
|
|
|
return final_path, True
|
|
|
|
|
else:
|
|
|
|
|
# Fallback — use albumartist for folder consistency (same as template)
|
|
|
|
|
artist_name_sanitized = _sanitize_filename(template_context['albumartist'])
|
|
|
|
|
album_name_sanitized = _sanitize_filename(album_info['album_name'])
|
|
|
|
|
artist_dir = os.path.join(transfer_dir, artist_name_sanitized)
|
|
|
|
|
album_folder_name = f"{artist_name_sanitized} - {album_name_sanitized}"
|
|
|
|
|
album_dir = os.path.join(artist_dir, album_folder_name)
|
|
|
|
|
if total_discs > 1:
|
|
|
|
|
album_dir = os.path.join(album_dir, f"{disc_label} {disc_number}")
|
|
|
|
|
os.makedirs(album_dir, exist_ok=True)
|
|
|
|
|
final_track_name_sanitized = _sanitize_filename(clean_track_name)
|
|
|
|
|
new_filename = f"{track_number:02d} - {final_track_name_sanitized}{file_ext}"
|
|
|
|
|
return os.path.join(album_dir, new_filename), True
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
# SINGLE MODE
|
|
|
|
|
clean_track_name = album_info.get('clean_track_name', 'Unknown Track') if album_info else 'Unknown Track'
|
|
|
|
|
if original_search.get('spotify_clean_title'):
|
|
|
|
|
clean_track_name = original_search['spotify_clean_title']
|
|
|
|
|
elif album_info and album_info.get('clean_track_name'):
|
|
|
|
|
clean_track_name = album_info['clean_track_name']
|
|
|
|
|
else:
|
|
|
|
|
clean_track_name = original_search.get('title', 'Unknown Track')
|
|
|
|
|
|
|
|
|
|
# Get structured artists list for collab artist handling (same as album/playlist modes)
|
|
|
|
|
_artists = original_search.get('artists') or track_info.get('artists') or []
|
|
|
|
|
# Extract iTunes artist ID for primary artist resolution (only for iTunes source)
|
|
|
|
|
_spotify_album = context.get('spotify_album', {})
|
|
|
|
|
_itunes_aid = None
|
|
|
|
|
_track_source = original_search.get('_source') or track_info.get('_source', '')
|
|
|
|
|
_is_itunes = _track_source == 'itunes' or (isinstance(spotify_artist, dict) and str(spotify_artist.get('id', '')).isdigit() and _track_source != 'deezer')
|
|
|
|
|
if _is_itunes and isinstance(spotify_artist, dict):
|
|
|
|
|
_aid = spotify_artist.get('id', '')
|
|
|
|
|
if str(_aid).isdigit():
|
|
|
|
|
_itunes_aid = str(_aid)
|
|
|
|
|
if not _itunes_aid and _spotify_album:
|
|
|
|
|
_ext = _spotify_album.get('external_urls', {})
|
|
|
|
|
if isinstance(_ext, dict) and _ext.get('itunes_artist_id'):
|
|
|
|
|
_itunes_aid = _ext['itunes_artist_id']
|
|
|
|
|
|
|
|
|
|
template_context = {
|
|
|
|
|
'artist': 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,
|
|
|
|
|
'title': clean_track_name,
|
|
|
|
|
'track_number': 1,
|
|
|
|
|
'disc_number': 1,
|
|
|
|
|
'year': year,
|
|
|
|
|
'quality': context.get('_audio_quality', ''),
|
|
|
|
|
'albumtype': album_type_display,
|
|
|
|
|
'_artists_list': _artists,
|
|
|
|
|
'_itunes_artist_id': _itunes_aid,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
folder_path, filename_base = _get_file_path_from_template(template_context, 'single_path')
|
|
|
|
|
if filename_base:
|
|
|
|
|
# folder_path may be '' for flat templates like "$artist - $title" (no subfolders)
|
|
|
|
|
if folder_path:
|
|
|
|
|
final_path = os.path.join(transfer_dir, folder_path, filename_base + file_ext)
|
|
|
|
|
os.makedirs(os.path.join(transfer_dir, folder_path), exist_ok=True)
|
|
|
|
|
else:
|
|
|
|
|
final_path = os.path.join(transfer_dir, filename_base + file_ext)
|
|
|
|
|
os.makedirs(transfer_dir, exist_ok=True)
|
|
|
|
|
return final_path, True
|
|
|
|
|
else:
|
|
|
|
|
# Fallback
|
|
|
|
|
artist_name_sanitized = _sanitize_filename(template_context['artist'])
|
|
|
|
|
final_track_name_sanitized = _sanitize_filename(clean_track_name)
|
|
|
|
|
artist_dir = os.path.join(transfer_dir, artist_name_sanitized)
|
|
|
|
|
single_folder_name = f"{artist_name_sanitized} - {final_track_name_sanitized}"
|
|
|
|
|
single_dir = os.path.join(artist_dir, single_folder_name)
|
|
|
|
|
os.makedirs(single_dir, exist_ok=True)
|
|
|
|
|
new_filename = f"{final_track_name_sanitized}{file_ext}"
|
|
|
|
|
return os.path.join(single_dir, new_filename), True
|
|
|
|
|
|
|
|
|
|
def _get_file_path_from_template_raw(template: str, context: dict) -> tuple:
|
|
|
|
|
"""
|
|
|
|
|
Build file path using a user-provided template string directly.
|
|
|
|
|
@ -18099,9 +17733,6 @@ def _get_file_path_from_template(context: dict, template_type: str = 'album_path
|
|
|
|
|
# ===================================================================
|
|
|
|
|
from mutagen import File as MutagenFile
|
|
|
|
|
from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TRCK, TCON, TPE2, TPOS, TXXX, APIC, UFID, TSRC, TBPM, TCOP, TPUB, TMED, TDOR
|
|
|
|
|
from mutagen.flac import FLAC, Picture
|
|
|
|
|
from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm
|
|
|
|
|
from mutagen.oggvorbis import OggVorbis
|
|
|
|
|
from mutagen.apev2 import APEv2, APENoHeaderError
|
|
|
|
|
import urllib.request
|
|
|
|
|
|
|
|
|
|
|