mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-06 12:48:03 +02:00
fix: improve lyrics error detection and add new donor
- Detect error JSON payloads from Apple Music and QQMusic proxies - Add lyricsHasUsableText validation for usable lyrics content - Skip empty word lines in synced lyrics parsing - Add ClearAll method to lyrics cache - Handle TimeoutException properly in track metadata screen - Add laflame to recent donors list
This commit is contained in:
+70
-1
@@ -224,6 +224,15 @@ func (c *lyricsCache) Size() int {
|
||||
return len(c.cache)
|
||||
}
|
||||
|
||||
func (c *lyricsCache) ClearAll() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
cleared := len(c.cache)
|
||||
c.cache = make(map[string]*lyricsCacheEntry)
|
||||
return cleared
|
||||
}
|
||||
|
||||
type LRCLibResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -399,7 +408,7 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st
|
||||
}
|
||||
|
||||
isValidResult := func(l *LyricsResponse) bool {
|
||||
return l != nil && (len(l.Lines) > 0 || l.Instrumental)
|
||||
return lyricsHasUsableText(l)
|
||||
}
|
||||
|
||||
// Try extension lyrics providers first
|
||||
@@ -619,6 +628,9 @@ func parseSyncedLyrics(syncedLyrics string) []LyricsLine {
|
||||
if len(matches) == 5 {
|
||||
startMs := lrcTimestampToMs(matches[1], matches[2], matches[3])
|
||||
words := strings.TrimSpace(matches[4])
|
||||
if words == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lines = append(lines, LyricsLine{
|
||||
StartTimeMs: startMs,
|
||||
@@ -639,6 +651,63 @@ func parseSyncedLyrics(syncedLyrics string) []LyricsLine {
|
||||
return lines
|
||||
}
|
||||
|
||||
func lyricsHasUsableText(lyrics *LyricsResponse) bool {
|
||||
if lyrics == nil {
|
||||
return false
|
||||
}
|
||||
if lyrics.Instrumental {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(lyrics.PlainLyrics) != "" {
|
||||
return true
|
||||
}
|
||||
for _, line := range lyrics.Lines {
|
||||
if strings.TrimSpace(line.Words) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// detectLyricsErrorPayload extracts human-readable error messages from
|
||||
// JSON payloads returned by lyrics proxies when no lyric is available.
|
||||
func detectLyricsErrorPayload(raw string) (string, bool) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" || !strings.HasPrefix(trimmed, "{") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
lyricsKeys := []string{"lyrics", "lyric", "lrc", "content", "lines", "syncedLyrics", "unsyncedLyrics"}
|
||||
hasLyricsKey := false
|
||||
for _, key := range lyricsKeys {
|
||||
if _, ok := payload[key]; ok {
|
||||
hasLyricsKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
errorKeys := []string{"message", "error", "detail", "reason"}
|
||||
for _, key := range errorKeys {
|
||||
if msg, ok := payload[key].(string); ok {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg != "" && !hasLyricsKey {
|
||||
return msg, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if success, ok := payload["success"].(bool); ok && !success && !hasLyricsKey {
|
||||
return "request unsuccessful", true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func lrcTimestampToMs(minutes, seconds, centiseconds string) int64 {
|
||||
min, _ := strconv.ParseInt(minutes, 10, 64)
|
||||
sec, _ := strconv.ParseInt(seconds, 10, 64)
|
||||
|
||||
@@ -333,6 +333,9 @@ func (c *AppleMusicClient) FetchLyrics(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(rawLyrics); isErrorPayload {
|
||||
return nil, fmt.Errorf("apple music proxy returned non-lyric payload: %s", errMsg)
|
||||
}
|
||||
|
||||
// Try to parse as pax format (word-by-word or line)
|
||||
lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord)
|
||||
|
||||
@@ -163,6 +163,9 @@ func (c *QQMusicClient) FetchLyrics(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(rawLyrics); isErrorPayload {
|
||||
return nil, fmt.Errorf("qqmusic proxy returned non-lyric payload: %s", errMsg)
|
||||
}
|
||||
|
||||
// Try to parse as pax format (word-by-word or line)
|
||||
lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord)
|
||||
|
||||
@@ -68,7 +68,9 @@ class DonatePage extends StatelessWidget {
|
||||
// Combined notice card
|
||||
Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.secondaryContainer.withValues(alpha: 0.3),
|
||||
color: colorScheme.secondaryContainer.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
@@ -98,7 +100,8 @@ class DonatePage extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
_NoticeLine(
|
||||
icon: Icons.block,
|
||||
text: 'Not selling early access, premium features, or paywalls',
|
||||
text:
|
||||
'Not selling early access, premium features, or paywalls',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -110,36 +113,40 @@ class DonatePage extends StatelessWidget {
|
||||
const SizedBox(height: 6),
|
||||
_NoticeLine(
|
||||
icon: Icons.favorite_border,
|
||||
text: 'Your support is the only way to keep this project alive',
|
||||
text:
|
||||
'Your support is the only way to keep this project alive',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
Divider(
|
||||
height: 24,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
color: colorScheme.outlineVariant.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
_NoticeLine(
|
||||
icon: Icons.history,
|
||||
text: 'Your name stays permanently in every version it was included in',
|
||||
text:
|
||||
'Your name stays permanently in every version it was included in',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_NoticeLine(
|
||||
icon: Icons.update,
|
||||
text: 'Supporter list is updated monthly and embedded in the app',
|
||||
text:
|
||||
'Supporter list is updated monthly and embedded in the app',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_NoticeLine(
|
||||
icon: Icons.cloud_off,
|
||||
text: 'No remote server -- everything is stored locally',
|
||||
text:
|
||||
'No remote server -- everything is stored locally',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -205,6 +212,7 @@ class _RecentDonorsCard extends StatelessWidget {
|
||||
_DonorTile(name: 'matt_3050', colorScheme: colorScheme),
|
||||
_DonorTile(name: 'Daniel', colorScheme: colorScheme),
|
||||
_DonorTile(name: '283Fabio', colorScheme: colorScheme),
|
||||
_DonorTile(name: 'laflame', colorScheme: colorScheme),
|
||||
_DonorTile(
|
||||
name: 'Elias el Autentico',
|
||||
colorScheme: colorScheme,
|
||||
@@ -414,9 +422,9 @@ class _NoticeLine extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -1519,17 +1520,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
}
|
||||
|
||||
// No embedded lyrics, fetch from online
|
||||
final result =
|
||||
await PlatformBridge.getLyricsLRCWithSource(
|
||||
_spotifyId ?? '',
|
||||
trackName,
|
||||
artistName,
|
||||
filePath: null, // Don't check file again
|
||||
durationMs: durationMs,
|
||||
).timeout(
|
||||
const Duration(seconds: 20),
|
||||
onTimeout: () => <String, dynamic>{'lyrics': '', 'source': ''},
|
||||
);
|
||||
final result = await PlatformBridge.getLyricsLRCWithSource(
|
||||
_spotifyId ?? '',
|
||||
trackName,
|
||||
artistName,
|
||||
filePath: null, // Don't check file again
|
||||
durationMs: durationMs,
|
||||
).timeout(const Duration(seconds: 20));
|
||||
|
||||
final lrcText = result['lyrics']?.toString() ?? '';
|
||||
final source = result['source']?.toString() ?? '';
|
||||
@@ -1561,13 +1558,17 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
});
|
||||
}
|
||||
}
|
||||
} on TimeoutException {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lyricsError = context.l10n.trackLyricsTimeout;
|
||||
_lyricsLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final errorMsg = e.toString().contains('TimeoutException')
|
||||
? context.l10n.trackLyricsTimeout
|
||||
: context.l10n.trackLyricsLoadFailed;
|
||||
setState(() {
|
||||
_lyricsError = errorMsg;
|
||||
_lyricsError = context.l10n.trackLyricsLoadFailed;
|
||||
_lyricsLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user