Skip to content

Commit fd6f5bf

Browse files
authored
Dev/v1.8.1 (#21)
* ADD: LyricsPlus API, word-sync highlighting, LyricsService dedup, _looksLikeLrc helper, YTM artist cleaner, terminal debug logs UPDATE: lyrics fallback chain to 3-tier, DesktopLyricsPane to StatefulWidget, wordLines sort order FIX: duplicate API calls from mobile and desktop, plainLyrics LRC not detected as synced, artist field showing bullet dot and album from YTM * fix: improve fallback artist extraction from runs * prep for v2.0.0
1 parent fa70c46 commit fd6f5bf

10 files changed

Lines changed: 1321 additions & 25 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,8 @@ flutter_console.log
8181
# App specific
8282
hongit_android.iml
8383
response.json
84-
PROJECT_STRUCTURE.md
84+
PROJECT_STRUCTURE.md
85+
86+
# : )
87+
claude/
88+
bot/

lib/core/utils/app_logger.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import 'dart:developer' as developer;
22

3+
import 'package:flutter/foundation.dart';
4+
35
class AppLogger {
46
static const String _name = 'Hongeet';
57

68
static void info(String message) {
79
developer.log(message, name: _name, level: 800);
10+
if (kDebugMode) debugPrint('[$_name/INFO] $message');
811
}
912

1013
static void warning(String message, {Object? error, StackTrace? stackTrace}) {
@@ -15,6 +18,10 @@ class AppLogger {
1518
error: error,
1619
stackTrace: stackTrace,
1720
);
21+
if (kDebugMode) {
22+
debugPrint('[$_name/WARN] $message');
23+
if (error != null) debugPrint(' error: $error');
24+
}
1825
}
1926

2027
static void error(String message, {Object? error, StackTrace? stackTrace}) {
@@ -25,5 +32,9 @@ class AppLogger {
2532
error: error,
2633
stackTrace: stackTrace,
2734
);
35+
if (kDebugMode) {
36+
debugPrint('[$_name/ERROR] $message');
37+
if (error != null) debugPrint(' error: $error');
38+
}
2839
}
2940
}

lib/data/api/lrclib_api.dart

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,38 @@ import 'dart:async';
22
import 'dart:convert';
33
import 'dart:io';
44

5+
import '../../core/utils/app_logger.dart';
6+
57
import 'package:http/http.dart' as http;
68
import 'package:path/path.dart' as p;
79
import 'package:path_provider/path_provider.dart';
810

11+
class WordSegment {
12+
final Duration start;
13+
final Duration end;
14+
final String text;
15+
16+
const WordSegment({
17+
required this.start,
18+
required this.end,
19+
required this.text,
20+
});
21+
}
22+
23+
class WordSyncedLine {
24+
final Duration start;
25+
final Duration end;
26+
final String fullText;
27+
final List<WordSegment> words;
28+
29+
const WordSyncedLine({
30+
required this.start,
31+
required this.end,
32+
required this.fullText,
33+
required this.words,
34+
});
35+
}
36+
937
class LyricLine {
1038
final Duration start;
1139
final String text;
@@ -23,6 +51,8 @@ class LrcLibLyrics {
2351
final String? syncedLyrics;
2452
final List<LyricLine> parsedLines;
2553

54+
final List<WordSyncedLine>? wordLines;
55+
2656
const LrcLibLyrics({
2757
required this.trackName,
2858
required this.artistName,
@@ -32,8 +62,11 @@ class LrcLibLyrics {
3262
required this.plainLyrics,
3363
required this.syncedLyrics,
3464
required this.parsedLines,
65+
this.wordLines,
3566
});
3667

68+
bool get hasWordSyncedLyrics => wordLines != null && wordLines!.isNotEmpty;
69+
3770
bool get hasSyncedLyrics => parsedLines.isNotEmpty;
3871
}
3972

@@ -103,7 +136,6 @@ class LrcLibApi {
103136
int? durationSeconds,
104137
String? album,
105138
}) async {
106-
// LRCLib fallback with multiple query variants
107139
final variants = _buildQueryVariants(
108140
title: title,
109141
artist: artist,
@@ -151,6 +183,12 @@ class LrcLibApi {
151183
final normalizedTitle = _normalize(_cleanTitleForQuery(title));
152184
final normalizedArtist = _normalize(_cleanArtistForQuery(artist));
153185

186+
AppLogger.info(
187+
'[LrcLib] Scoring ${deduped.length} candidates for '
188+
'"$title" by "$artist" '
189+
'(query title: "$normalizedTitle", query artist: "$normalizedArtist")',
190+
);
191+
154192
Map<String, dynamic>? best;
155193
var bestScore = -1 << 30;
156194
for (final item in deduped) {
@@ -160,13 +198,32 @@ class LrcLibApi {
160198
normalizedArtist: normalizedArtist,
161199
durationSeconds: durationSeconds,
162200
);
201+
202+
final candidateTitle = (item['trackName'] ?? '').toString().trim();
203+
final candidateArtist = (item['artistName'] ?? '').toString().trim();
204+
final hasSynced =
205+
((item['syncedLyrics'] ?? '').toString().trim()).isNotEmpty;
206+
AppLogger.info(
207+
'[LrcLib] score=$score '
208+
'"$candidateTitle" — "$candidateArtist" '
209+
'(synced: $hasSynced)',
210+
);
211+
163212
if (score > bestScore) {
164213
bestScore = score;
165214
best = item;
166215
}
167216
}
168217

169218
if (best == null) return null;
219+
220+
final pickedTitle = (best['trackName'] ?? '').toString().trim();
221+
final pickedArtist = (best['artistName'] ?? '').toString().trim();
222+
AppLogger.info(
223+
'[LrcLib] Picked: "$pickedTitle" — "$pickedArtist" '
224+
'(score: $bestScore)',
225+
);
226+
170227
return _parseLyrics(best);
171228
}
172229

@@ -316,7 +373,16 @@ class LrcLibApi {
316373

317374
static LrcLibLyrics _parseLyrics(Map<String, dynamic> raw) {
318375
final plainLyrics = (raw['plainLyrics'] ?? '').toString();
319-
final syncedLyrics = (raw['syncedLyrics'] ?? '').toString().trim();
376+
var syncedLyrics = (raw['syncedLyrics'] ?? '').toString().trim();
377+
378+
if (syncedLyrics.isEmpty && _looksLikeLrc(plainLyrics)) {
379+
AppLogger.info(
380+
'[LrcLib] plainLyrics looks like LRC — promoting to syncedLyrics for '
381+
'"${(raw["trackName"] ?? "").toString().trim()}"',
382+
);
383+
syncedLyrics = plainLyrics.trim();
384+
}
385+
320386
final parsed = _parseSyncedLyrics(
321387
syncedLyrics.isEmpty ? null : syncedLyrics,
322388
);
@@ -332,6 +398,18 @@ class LrcLibApi {
332398
);
333399
}
334400

401+
static bool _looksLikeLrc(String text) {
402+
if (text.trim().isEmpty) return false;
403+
final timestampRegex = RegExp(r'^\[\d{1,2}:\d{2}');
404+
var matches = 0;
405+
for (final line in text.split('\n')) {
406+
if (timestampRegex.hasMatch(line.trim())) {
407+
if (++matches >= 3) return true;
408+
}
409+
}
410+
return false;
411+
}
412+
335413
static List<LyricLine> _parseSyncedLyrics(String? syncedLyrics) {
336414
if (syncedLyrics == null || syncedLyrics.trim().isEmpty) {
337415
return const <LyricLine>[];
@@ -673,6 +751,7 @@ class _CachedLyrics {
673751
plainLyrics: (lyricsMap['plainLyrics'] ?? '').toString(),
674752
syncedLyrics: synced.isEmpty ? null : synced,
675753
parsedLines: parsed,
754+
// wordLines intentionally omitted
676755
);
677756
return _CachedLyrics(lyrics: lyrics, cachedAt: cachedAt);
678757
}

lib/data/api/lyrics_service.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'package:hongit/core/utils/app_logger.dart';
2+
import 'package:hongit/data/api/lrclib_api.dart';
3+
import 'package:hongit/data/api/lyricsplus_api.dart';
4+
5+
/// Single entry point for lyrics fetching used by both mobile and desktop UI
6+
class LyricsService {
7+
LyricsService._();
8+
9+
static final Map<String, Future<LrcLibLyrics?>> _inFlight = {};
10+
11+
static Future<LrcLibLyrics?> fetchBestLyrics({
12+
required String title,
13+
required String artist,
14+
int? durationSeconds,
15+
String? album,
16+
}) {
17+
final key = '$title|$artist|${durationSeconds ?? -1}';
18+
final existing = _inFlight[key];
19+
if (existing != null) return existing;
20+
21+
final future = _doFetch(
22+
title: title,
23+
artist: artist,
24+
durationSeconds: durationSeconds,
25+
album: album,
26+
);
27+
_inFlight[key] = future;
28+
return future.whenComplete(() => _inFlight.remove(key));
29+
}
30+
31+
static Future<LrcLibLyrics?> _doFetch({
32+
required String title,
33+
required String artist,
34+
int? durationSeconds,
35+
String? album,
36+
}) async {
37+
AppLogger.info(
38+
'[Lyrics] Fetching "$title" by "$artist" — trying LyricsPlus first',
39+
);
40+
final plusResult = await LyricsPlusApi.fetchBestLyrics(
41+
title: title,
42+
artist: artist,
43+
durationSeconds: durationSeconds,
44+
album: album,
45+
);
46+
if (plusResult != null) {
47+
AppLogger.info(
48+
'[Lyrics] Source: LyricsPlus — '
49+
'${plusResult.parsedLines.length} lines for "$title"',
50+
);
51+
return plusResult;
52+
}
53+
AppLogger.info(
54+
'[Lyrics] LyricsPlus returned null for "$title" — falling back to LrcLib',
55+
);
56+
final lrcResult = await LrcLibApi.fetchBestLyrics(
57+
title: title,
58+
artist: artist,
59+
durationSeconds: durationSeconds,
60+
album: album,
61+
);
62+
if (lrcResult != null) {
63+
AppLogger.info(
64+
'[Lyrics] Source: LrcLib — '
65+
'${lrcResult.parsedLines.length} lines for "$title" '
66+
'(synced: ${lrcResult.hasSyncedLyrics})',
67+
);
68+
} else {
69+
AppLogger.warning(
70+
'[Lyrics] No lyrics found from any source for "$title" by "$artist"',
71+
);
72+
}
73+
return lrcResult;
74+
}
75+
}

0 commit comments

Comments
 (0)