Fix .lrc files written without timestamps for plain lyrics
Plain (unsynced) lyrics were being saved with .lrc extension despite having no timestamps, making them invalid for Plex and other players that expect LRC format. Synced lyrics now write as .lrc, plain lyrics write as .txt. Both types still get embedded in audio file tags. Updated all file move/rename operations to handle .txt sidecars alongside .lrc.
This commit is contained in:
parent
c5652b0d4b
commit
fde1a7d77e
3 changed files with 61 additions and 42 deletions
|
|
@ -50,10 +50,11 @@ class LyricsClient:
|
|||
try:
|
||||
# Generate LRC file path (same name as audio file, .lrc extension)
|
||||
lrc_path = os.path.splitext(audio_file_path)[0] + '.lrc'
|
||||
txt_path = os.path.splitext(audio_file_path)[0] + '.txt'
|
||||
|
||||
# Skip if LRC file already exists
|
||||
if os.path.exists(lrc_path):
|
||||
logger.debug(f"LRC file already exists: {os.path.basename(lrc_path)}")
|
||||
# Skip if lyrics file already exists (either .lrc or .txt)
|
||||
if os.path.exists(lrc_path) or os.path.exists(txt_path):
|
||||
logger.debug(f"Lyrics file already exists for: {os.path.basename(audio_file_path)}")
|
||||
return True
|
||||
|
||||
# Fetch lyrics from LRClib
|
||||
|
|
@ -95,27 +96,31 @@ class LyricsClient:
|
|||
logger.debug(f"No lyrics found for: {artist_name} - {track_name}")
|
||||
return False
|
||||
|
||||
# Prefer synced lyrics, fallback to plain text
|
||||
# LRClib API uses synced_lyrics and plain_lyrics attributes
|
||||
lrc_content = getattr(lyrics_data, 'synced_lyrics', None) or getattr(lyrics_data, 'plain_lyrics', None)
|
||||
# LRClib API provides synced_lyrics (timestamped) and plain_lyrics (text only)
|
||||
synced = getattr(lyrics_data, 'synced_lyrics', None)
|
||||
plain = getattr(lyrics_data, 'plain_lyrics', None)
|
||||
|
||||
logger.debug(f"Synced lyrics available: {bool(getattr(lyrics_data, 'synced_lyrics', None))}")
|
||||
logger.debug(f"Plain lyrics available: {bool(getattr(lyrics_data, 'plain_lyrics', None))}")
|
||||
logger.debug(f"LRC content found: {bool(lrc_content)}")
|
||||
logger.debug(f"Synced lyrics available: {bool(synced)}")
|
||||
logger.debug(f"Plain lyrics available: {bool(plain)}")
|
||||
|
||||
if not lrc_content:
|
||||
if not synced and not plain:
|
||||
logger.debug(f"No usable lyrics content for: {artist_name} - {track_name}")
|
||||
return False
|
||||
|
||||
# Write LRC file
|
||||
with open(lrc_path, 'w', encoding='utf-8') as f:
|
||||
f.write(lrc_content)
|
||||
|
||||
# Embed lyrics directly in audio file tags (Navidrome/Jellyfin read these)
|
||||
self._embed_lyrics(audio_file_path, lrc_content)
|
||||
|
||||
lyrics_type = "synced" if getattr(lyrics_data, 'synced_lyrics', None) else "plain"
|
||||
logger.info(f"✅ Created {lyrics_type} LRC file + embedded: {os.path.basename(lrc_path)}")
|
||||
if synced:
|
||||
# Synced lyrics have timestamps → valid .lrc format
|
||||
with open(lrc_path, 'w', encoding='utf-8') as f:
|
||||
f.write(synced)
|
||||
# Embed synced lyrics in audio tags
|
||||
self._embed_lyrics(audio_file_path, synced)
|
||||
logger.info(f"✅ Created synced LRC + embedded: {os.path.basename(lrc_path)}")
|
||||
else:
|
||||
# Plain lyrics only → write as .txt (not .lrc, which requires timestamps)
|
||||
with open(txt_path, 'w', encoding='utf-8') as f:
|
||||
f.write(plain)
|
||||
# Still embed plain lyrics in audio tags (players can display unsynced lyrics)
|
||||
self._embed_lyrics(audio_file_path, plain)
|
||||
logger.info(f"✅ Created plain lyrics .txt + embedded: {os.path.basename(txt_path)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -15393,11 +15393,12 @@ def _downsample_hires_flac(final_path, context):
|
|||
try:
|
||||
os.rename(final_path, new_path)
|
||||
print(f"📝 [Downsample] Renamed: {os.path.basename(final_path)} → {new_basename}")
|
||||
# Rename matching LRC file if it exists
|
||||
old_lrc = os.path.splitext(final_path)[0] + '.lrc'
|
||||
if os.path.isfile(old_lrc):
|
||||
new_lrc = os.path.splitext(new_path)[0] + '.lrc'
|
||||
os.rename(old_lrc, new_lrc)
|
||||
# Rename matching lyrics sidecar file if it exists (.lrc or .txt)
|
||||
for lyrics_ext in ('.lrc', '.txt'):
|
||||
old_lyrics = os.path.splitext(final_path)[0] + lyrics_ext
|
||||
if os.path.isfile(old_lyrics):
|
||||
new_lyrics = os.path.splitext(new_path)[0] + lyrics_ext
|
||||
os.rename(old_lyrics, new_lyrics)
|
||||
return new_path
|
||||
except Exception as rename_err:
|
||||
print(f"⚠️ [Downsample] Could not rename file: {rename_err}")
|
||||
|
|
@ -15551,15 +15552,16 @@ def _create_lossy_copy(final_path):
|
|||
if test_audio is not None:
|
||||
os.remove(final_path)
|
||||
print(f"🔥 [Blasphemy Mode] Deleted original: {os.path.basename(final_path)}")
|
||||
# Rename LRC file to match the output filename
|
||||
flac_lrc = os.path.splitext(final_path)[0] + '.lrc'
|
||||
if os.path.isfile(flac_lrc):
|
||||
out_lrc = os.path.splitext(out_path)[0] + '.lrc'
|
||||
try:
|
||||
os.rename(flac_lrc, out_lrc)
|
||||
print(f"🔥 [Blasphemy Mode] Renamed LRC: {os.path.basename(flac_lrc)} -> {os.path.basename(out_lrc)}")
|
||||
except Exception as lrc_err:
|
||||
print(f"⚠️ [Blasphemy Mode] Could not rename LRC: {lrc_err}")
|
||||
# Rename lyrics sidecar file to match the output filename
|
||||
for lyrics_ext in ('.lrc', '.txt'):
|
||||
src_lyrics = os.path.splitext(final_path)[0] + lyrics_ext
|
||||
if os.path.isfile(src_lyrics):
|
||||
dst_lyrics = os.path.splitext(out_path)[0] + lyrics_ext
|
||||
try:
|
||||
os.rename(src_lyrics, dst_lyrics)
|
||||
print(f"🔥 [Blasphemy Mode] Renamed {lyrics_ext}: {os.path.basename(src_lyrics)} -> {os.path.basename(dst_lyrics)}")
|
||||
except Exception as lrc_err:
|
||||
print(f"⚠️ [Blasphemy Mode] Could not rename {lyrics_ext}: {lrc_err}")
|
||||
return out_path
|
||||
else:
|
||||
print(f"⚠️ [Blasphemy Mode] Output failed audio validation, keeping original: {os.path.basename(final_path)}")
|
||||
|
|
@ -18802,15 +18804,16 @@ def _execute_retag(group_id, album_id):
|
|||
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||
_safe_move_file(current_file_path, new_path)
|
||||
|
||||
# Move .lrc file alongside audio file if it exists
|
||||
old_lrc = os.path.splitext(current_file_path)[0] + '.lrc'
|
||||
if os.path.exists(old_lrc):
|
||||
new_lrc = os.path.splitext(new_path)[0] + '.lrc'
|
||||
try:
|
||||
_safe_move_file(old_lrc, new_lrc)
|
||||
print(f"📝 [Retag] Moved .lrc file alongside audio")
|
||||
except Exception as lrc_err:
|
||||
print(f"⚠️ [Retag] Failed to move .lrc file: {lrc_err}")
|
||||
# Move lyrics sidecar file alongside audio file if it exists
|
||||
for lyrics_ext in ('.lrc', '.txt'):
|
||||
old_lyrics = os.path.splitext(current_file_path)[0] + lyrics_ext
|
||||
if os.path.exists(old_lyrics):
|
||||
new_lyrics = os.path.splitext(new_path)[0] + lyrics_ext
|
||||
try:
|
||||
_safe_move_file(old_lyrics, new_lyrics)
|
||||
print(f"📝 [Retag] Moved {lyrics_ext} file alongside audio")
|
||||
except Exception as lrc_err:
|
||||
print(f"⚠️ [Retag] Failed to move {lyrics_ext} file: {lrc_err}")
|
||||
|
||||
# Remove old cover.jpg if directory changed and old dir is now empty of audio
|
||||
new_dir = os.path.dirname(new_path)
|
||||
|
|
@ -19204,6 +19207,16 @@ def get_version_info():
|
|||
"title": "What's New in SoulSync",
|
||||
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
||||
"sections": [
|
||||
{
|
||||
"title": "🔧 Fix .LRC Files Written Without Timestamps",
|
||||
"description": "Plain lyrics now saved as .txt instead of invalid .lrc files",
|
||||
"features": [
|
||||
"• Synced (timestamped) lyrics → .lrc file — valid format for Plex, Navidrome, Jellyfin",
|
||||
"• Plain (unsynced) lyrics → .txt file — no longer written with incorrect .lrc extension",
|
||||
"• Lyrics still embedded in audio file tags regardless of type (players can display both)",
|
||||
"• File move/rename operations updated to handle both .lrc and .txt sidecars"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "🔧 Fix Collaborative Album Artist Not Applied to Singles (#215)",
|
||||
"description": "Single path template now respects the First Listed Artist setting",
|
||||
|
|
|
|||
|
|
@ -3403,6 +3403,7 @@ function closeHelperSearch() {
|
|||
const WHATS_NEW = {
|
||||
'2.1': [
|
||||
// Newest features first
|
||||
{ title: 'Fix Invalid .LRC Lyrics Files', desc: 'Plain lyrics now saved as .txt — only synced (timestamped) lyrics get the .lrc extension' },
|
||||
{ title: 'Fix Collab Artist on Singles', desc: 'Single/playlist path templates now respect First Listed Artist setting — $albumartist available for all template types' },
|
||||
{ title: 'Fix Enrichment Breaking Manual Matches', desc: 'Enriching a manually matched artist no longer reverts status to not_found — uses stored ID for direct lookup' },
|
||||
{ title: 'Fix Spotify OAuth Empty Response', desc: 'OAuth callback server now always sends a response in Docker — added health check and proper logging' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue