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.
pull/253/head
Broque Thomas 2 months ago
parent c5652b0d4b
commit fde1a7d77e

@ -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…
Cancel
Save