Embed lyrics directly in audio file tags
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.
This commit is contained in:
parent
f68cae64a7
commit
7cff379aa7
3 changed files with 49 additions and 1 deletions
|
|
@ -111,8 +111,11 @@ class LyricsClient:
|
||||||
with open(lrc_path, 'w', encoding='utf-8') as f:
|
with open(lrc_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(lrc_content)
|
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"
|
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
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -120,5 +123,40 @@ class LyricsClient:
|
||||||
return False
|
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
|
# Global instance for easy import
|
||||||
lyrics_client = LyricsClient()
|
lyrics_client = LyricsClient()
|
||||||
|
|
@ -19009,6 +19009,15 @@ def get_version_info():
|
||||||
"title": "What's New in SoulSync",
|
"title": "What's New in SoulSync",
|
||||||
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
||||||
"sections": [
|
"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",
|
"title": "🔧 Fix AcoustID False Positives for Non-English Tracks",
|
||||||
"description": "AcoustID no longer quarantines correct files when titles are in different languages",
|
"description": "AcoustID no longer quarantines correct files when titles are in different languages",
|
||||||
|
|
|
||||||
|
|
@ -3403,6 +3403,7 @@ function closeHelperSearch() {
|
||||||
const WHATS_NEW = {
|
const WHATS_NEW = {
|
||||||
'2.1': [
|
'2.1': [
|
||||||
// Newest features first
|
// 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 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: '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' },
|
{ 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' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue