From 7cff379aa7ec1a151813764a8ffb8fcedd1d8c69 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:15:26 -0700 Subject: [PATCH] Embed lyrics directly in audio file tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lyrics from LRClib are now embedded in audio file tags (USLT for MP3, lyrics for FLAC/OGG, ©lyr for M4A) in addition to the .lrc sidecar file. Navidrome, Jellyfin, and Plex can read embedded lyrics but not all support .lrc files. Embedding is best-effort — if it fails, the .lrc file still exists. Only adds to existing tags, never creates tags from scratch. --- core/lyrics_client.py | 40 +++++++++++++++++++++++++++++++++++++++- web_server.py | 9 +++++++++ webui/static/helper.js | 1 + 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/core/lyrics_client.py b/core/lyrics_client.py index 029bd027..4def5b75 100644 --- a/core/lyrics_client.py +++ b/core/lyrics_client.py @@ -111,8 +111,11 @@ class LyricsClient: 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: {os.path.basename(lrc_path)}") + logger.info(f"✅ Created {lyrics_type} LRC file + embedded: {os.path.basename(lrc_path)}") return True except Exception as e: @@ -120,5 +123,40 @@ class LyricsClient: return False + def _embed_lyrics(self, audio_file_path: str, lyrics_text: str): + """Embed lyrics directly into audio file tags.""" + try: + from mutagen import File as MutagenFile + from mutagen.flac import FLAC + from mutagen.oggvorbis import OggVorbis + from mutagen.mp4 import MP4 + from mutagen.id3 import ID3, USLT + + audio = MutagenFile(audio_file_path) + if audio is None: + return + + if audio.tags is None: + return # Don't create tags just for lyrics + + if isinstance(audio.tags, ID3): + audio.tags.delall('USLT') + audio.tags.add(USLT(encoding=3, lang='eng', desc='', text=lyrics_text)) + audio.save(v1=0, v2_version=4) + elif isinstance(audio, (FLAC, OggVorbis)) or type(audio).__name__ == 'OggOpus': + audio['lyrics'] = [lyrics_text] + if isinstance(audio, FLAC): + audio.save(deleteid3=True) + else: + audio.save() + elif isinstance(audio, MP4): + audio['\xa9lyr'] = [lyrics_text] + audio.save() + + logger.debug(f"Embedded lyrics in: {os.path.basename(audio_file_path)}") + except Exception as e: + logger.warning(f"Could not embed lyrics in {os.path.basename(audio_file_path)}: {e}") + + # Global instance for easy import lyrics_client = LyricsClient() \ No newline at end of file diff --git a/web_server.py b/web_server.py index 630cda85..e2616d50 100644 --- a/web_server.py +++ b/web_server.py @@ -19009,6 +19009,15 @@ def get_version_info(): "title": "What's New in SoulSync", "subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes", "sections": [ + { + "title": "🎵 Embedded Lyrics in Audio Files", + "description": "Lyrics are now embedded directly in audio file tags alongside the .lrc sidecar file", + "features": [ + "• Lyrics embedded as USLT (MP3), lyrics (FLAC/OGG), or ©lyr (M4A) tags", + "• Navidrome, Jellyfin, and Plex can now display lyrics without .lrc file support", + "• .lrc sidecar files are still created for compatibility with other players" + ] + }, { "title": "🔧 Fix AcoustID False Positives for Non-English Tracks", "description": "AcoustID no longer quarantines correct files when titles are in different languages", diff --git a/webui/static/helper.js b/webui/static/helper.js index ea4b5f44..c1f073da 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3403,6 +3403,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.1': [ // Newest features first + { title: 'Embedded Lyrics', desc: 'Lyrics now embedded directly in audio file tags — Navidrome, Jellyfin, and Plex can display them' }, { title: 'Fix AcoustID False Positives', desc: 'High-confidence fingerprints no longer quarantine correct files when titles are in different languages' }, { title: 'Fix Junk Tags Surviving', desc: 'Soulseek source tags are now wiped to disk immediately — no more album fragmentation from partial metadata' }, { title: 'Watch All Preview Modal', desc: 'Watch All Unwatched now opens a modal showing which artists will be added before confirming', page: 'library', selector: '#library-watchlist-all-btn' },