Skip to content

Commit f2f5b39

Browse files
committed
feat: Enhance lyrics handling with improved tagging and compatibility fixes
1 parent a602456 commit f2f5b39

3 files changed

Lines changed: 100 additions & 17 deletions

File tree

backend/api/services/audio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mutagen.flac import FLAC, Picture
77
from mutagen.mp4 import MP4, MP4Cover
88
from mutagen.mp3 import MP3
9-
from mutagen.id3 import ID3, APIC, ID3NoHeaderError
9+
from mutagen.id3 import ID3, APIC, ID3NoHeaderError, Encoding
1010
from mutagen.easyid3 import EasyID3
1111
from mutagen.oggopus import OggOpus
1212

@@ -383,7 +383,7 @@ async def write_mp3_metadata(filepath: Path, metadata: dict):
383383
except Exception as e:
384384
log_warning(f"Failed to add custom TXXX tags: {e}")
385385

386-
await fetch_and_store_lyrics(filepath, metadata, None)
386+
await fetch_and_store_lyrics(filepath, metadata, None, is_mp3=True)
387387

388388
if metadata.get('cover_url'):
389389
try:

backend/api/services/lyrics.py

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
import shutil
55
from lyrics_client import lyrics_client
66

7-
async def fetch_and_store_lyrics(filepath: Path, metadata: dict, audio_file=None):
7+
async def fetch_and_store_lyrics(filepath: Path, metadata: dict, audio_file=None, is_mp3=False):
8+
"""
9+
Fetch and store lyrics for an audio file.
10+
- Synced lyrics: Save as .lrc file + SYNCEDLYRICS tag (FLAC/Opus) or SYLT (MP3)
11+
- Plain lyrics: Embed in LYRICS tag (FLAC/Opus) or USLT (MP3)
12+
"""
813
if metadata.get('title') and metadata.get('artist'):
914
try:
1015
log_info("Fetching lyrics...")
@@ -18,25 +23,93 @@ async def fetch_and_store_lyrics(filepath: Path, metadata: dict, audio_file=None
1823
if lyrics_result:
1924
if lyrics_result.synced_lyrics:
2025
metadata['synced_lyrics'] = lyrics_result.synced_lyrics
21-
log_success("Synced lyrics found (will save to .lrc)")
26+
# Save synced lyrics to .lrc sidecar file (most compatible)
27+
lrc_path = filepath.with_suffix('.lrc')
28+
try:
29+
with open(lrc_path, 'w', encoding='utf-8') as f:
30+
f.write(lyrics_result.synced_lyrics)
31+
log_success(f"Saved synced lyrics to {lrc_path.name}")
32+
except Exception as e:
33+
log_warning(f"Failed to save .lrc file: {e}")
34+
35+
# Embed in tags (SYNCEDLYRICS for FLAC/Opus, SYLT for MP3)
36+
if is_mp3:
37+
try:
38+
from mutagen.mp3 import MP3
39+
from mutagen.id3 import ID3, SYLT, Encoding
40+
audio = MP3(str(filepath), ID3=ID3)
41+
if audio.tags is None:
42+
audio.add_tags()
43+
44+
# Parse LRC format and create SYLT
45+
lines = []
46+
for line in lyrics_result.synced_lyrics.split('\n'):
47+
# LRC format: [mm:ss.xx]text
48+
if line.startswith('[') and ']' in line:
49+
timestamp_part = line[1:line.index(']')]
50+
text_part = line[line.index(']')+1:]
51+
if ':' in timestamp_part and text_part.strip():
52+
try:
53+
parts = timestamp_part.split(':')
54+
minutes = int(parts[0])
55+
seconds = float(parts[1])
56+
milliseconds = int((minutes * 60 + seconds) * 1000)
57+
lines.append((text_part, milliseconds))
58+
except (ValueError, IndexError):
59+
continue
60+
61+
if lines:
62+
audio.tags.delall('SYLT')
63+
audio.tags.add(SYLT(
64+
encoding=Encoding.UTF8,
65+
lang='eng',
66+
format=2, # milliseconds
67+
type=1, # lyrics
68+
text=lines
69+
))
70+
audio.save()
71+
log_success("Embedded synced lyrics in SYLT frame")
72+
except Exception as e:
73+
log_warning(f"Failed to embed MP3 SYLT: {e}")
74+
elif audio_file:
75+
try:
76+
audio_file['SYNCEDLYRICS'] = lyrics_result.synced_lyrics
77+
log_success("Embedded synced lyrics in SYNCEDLYRICS tag")
78+
except Exception as e:
79+
log_warning(f"Failed to embed synced lyrics tag: {e}")
80+
2281
elif lyrics_result.plain_lyrics:
2382
metadata['plain_lyrics'] = lyrics_result.plain_lyrics
24-
log_success("Plain lyrics found (will save to .txt)")
83+
84+
# Embed plain lyrics
85+
if is_mp3:
86+
try:
87+
from mutagen.mp3 import MP3
88+
from mutagen.id3 import ID3, USLT, Encoding
89+
audio = MP3(str(filepath), ID3=ID3)
90+
if audio.tags is None:
91+
audio.add_tags()
92+
93+
audio.tags.delall('USLT')
94+
audio.tags.add(USLT(
95+
encoding=Encoding.UTF8,
96+
lang='eng',
97+
desc='',
98+
text=lyrics_result.plain_lyrics
99+
))
100+
audio.save()
101+
log_success("Embedded plain lyrics in USLT frame")
102+
except Exception as e:
103+
log_warning(f"Failed to embed MP3 USLT: {e}")
104+
elif audio_file:
105+
try:
106+
audio_file['LYRICS'] = lyrics_result.plain_lyrics
107+
log_success("Embedded plain lyrics in LYRICS tag")
108+
except Exception as e:
109+
log_warning(f"Failed to embed plain lyrics tag: {e}")
25110

26111
except Exception as e:
27112
log_warning(f"Failed to fetch lyrics: {e}")
28-
29-
if audio_file and metadata.get('synced_lyrics'):
30-
try:
31-
lyrics_text = metadata['synced_lyrics']
32-
33-
for i, line in enumerate(lyrics_text.split('\n')):
34-
if line.strip():
35-
audio_file[f'LYRICS_LINE_{i+1}'] = line.strip()
36-
37-
log_success(f"Embedded {len(lyrics_text.splitlines())} lines of lyrics")
38-
except Exception as e:
39-
log_warning(f"Failed to embed lyrics: {e}")
40113

41114
async def embed_lyrics_with_ffmpeg(filepath: Path, metadata: dict):
42115
"""Embed lyrics into the audio file using FFmpeg"""

frontend/src/data/releaseNotes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
export const releaseNotes = [
2+
{
3+
version: "1.3.1",
4+
date: "2026-01-01",
5+
title: "Standardized Lyrics & Compatibility Fixes",
6+
changes: [
7+
"Improved Tagging: Synced lyrics are now correctly embedded in SYNCEDLYRICS (FLAC/Opus) and SYLT (MP3) tags.",
8+
"Enhanced Plain Lyrics: Unsynced lyrics now use standard LYRICS (FLAC/Opus) and USLT (MP3) metadata frames.",
9+
"Fixed non-standard lyrics tagging that caused issues with external media servers."
10+
]
11+
},
212
{
313
version: "1.3.0",
414
date: "2026-01-01",

0 commit comments

Comments
 (0)