🐛 Fix lyrics fetch

This commit is contained in:
2025-12-16 22:02:59 +08:00
parent 9f44786d6d
commit ac78ac002b
5 changed files with 119 additions and 26 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_media_metadata/flutter_media_metadata.dart'; import 'package:flutter_media_metadata/flutter_media_metadata.dart';
import 'package:groovybox/providers/db_provider.dart'; import 'package:groovybox/providers/db_provider.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@@ -69,7 +70,7 @@ class TrackRepository extends _$TrackRepository {
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
); );
} catch (e) { } catch (e) {
print('Error importing file $path: $e'); debugPrint('Error importing file $path: $e');
// Continue to next file // Continue to next file
} }
} }
@@ -112,7 +113,7 @@ class TrackRepository extends _$TrackRepository {
try { try {
await file.delete(); await file.delete();
} catch (e) { } catch (e) {
print("Error deleting file: $e"); debugPrint("Error deleting file: $e");
} }
} }
@@ -123,7 +124,7 @@ class TrackRepository extends _$TrackRepository {
try { try {
await artFile.delete(); await artFile.delete();
} catch (e) { } catch (e) {
print("Error deleting art: $e"); debugPrint("Error deleting art: $e");
} }
} }
} }

View File

@@ -15,10 +15,10 @@ class Lyrics {
} }
/// Abstract base class for LRC providers /// Abstract base class for LRC providers
abstract class LRCProvider { abstract class LrcProvider {
late final http.Client session; late final http.Client session;
LRCProvider() { LrcProvider() {
session = http.Client(); session = http.Client();
} }
@@ -29,7 +29,7 @@ abstract class LRCProvider {
} }
/// Musixmatch LRC provider /// Musixmatch LRC provider
class MusixmatchProvider extends LRCProvider { class MusixmatchProvider extends LrcProvider {
static const String rootUrl = "https://apic-desktop.musixmatch.com/ws/1.1/"; static const String rootUrl = "https://apic-desktop.musixmatch.com/ws/1.1/";
final String? lang; final String? lang;
@@ -38,6 +38,7 @@ class MusixmatchProvider extends LRCProvider {
MusixmatchProvider({this.lang, this.enhanced = false}); MusixmatchProvider({this.lang, this.enhanced = false});
@override
String get name => 'Musixmatch'; String get name => 'Musixmatch';
Future<http.Response> _get( Future<http.Response> _get(
@@ -178,7 +179,7 @@ class MusixmatchProvider extends LRCProvider {
} }
/// NetEase provider /// NetEase provider
class NetEaseProvider extends LRCProvider { class NetEaseProvider extends LrcProvider {
static const String apiEndpointMetadata = static const String apiEndpointMetadata =
"https://music.163.com/api/search/pc"; "https://music.163.com/api/search/pc";
static const String apiEndpointLyrics = static const String apiEndpointLyrics =
@@ -211,7 +212,7 @@ class NetEaseProvider extends LRCProvider {
headers: {"cookie": cookie}, headers: {"cookie": cookie},
); );
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
final lrc = Lyrics(plain: data["lrc"]["lyric"]); final lrc = Lyrics(synced: data["lrc"]["lyric"]);
return lrc; return lrc;
} }

View File

@@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:groovybox/data/db.dart' as db; import 'package:groovybox/data/db.dart' as db;
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:groovybox/logic/lrc_providers.dart'; import 'package:groovybox/logic/lrc_providers.dart';
import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/lyrics_parser.dart';
import 'package:groovybox/providers/db_provider.dart'; import 'package:groovybox/providers/db_provider.dart';
import 'package:groovybox/ui/screens/player_screen.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -18,14 +20,18 @@ class LyricsFetcher extends _$LyricsFetcher {
Future<void> fetchLyricsForTrack({ Future<void> fetchLyricsForTrack({
required int trackId, required int trackId,
required String searchTerm, required String searchTerm,
required LRCProvider provider, required LrcProvider provider,
required String trackPath, required String trackPath,
}) async { }) async {
debugPrint(
'Fetching lyrics for track $trackId with search term: $searchTerm',
);
state = state.copyWith(isLoading: true, error: null); state = state.copyWith(isLoading: true, error: null);
try { try {
final lyrics = await provider.getLrc(searchTerm); final lyrics = await provider.getLrc(searchTerm);
if (lyrics == null) { if (lyrics == null) {
debugPrint('No lyrics found from ${provider.name}');
state = state.copyWith( state = state.copyWith(
isLoading: false, isLoading: false,
error: 'No lyrics found from ${provider.name}', error: 'No lyrics found from ${provider.name}',
@@ -52,17 +58,26 @@ class LyricsFetcher extends _$LyricsFetcher {
..where((t) => t.id.equals(trackId))) ..where((t) => t.id.equals(trackId)))
.write(db.TracksCompanion(lyrics: drift.Value(lyricsJson))); .write(db.TracksCompanion(lyrics: drift.Value(lyricsJson)));
debugPrint('Updated database with lyrics for track $trackId');
// Invalidate the track provider to refresh the UI
ref.invalidate(trackByPathProvider(trackPath));
debugPrint('Invalidated track provider for $trackPath');
state = state.copyWith( state = state.copyWith(
isLoading: false, isLoading: false,
successMessage: 'Lyrics fetched from ${provider.name}', successMessage: 'Lyrics fetched from ${provider.name}',
); );
} else { } else {
debugPrint('Failed to parse lyrics');
state = state.copyWith( state = state.copyWith(
isLoading: false, isLoading: false,
error: 'Failed to parse lyrics', error: 'Failed to parse lyrics',
); );
} }
} catch (e) { } catch (e) {
debugPrint('Error fetching lyrics: $e');
state = state.copyWith( state = state.copyWith(
isLoading: false, isLoading: false,
error: 'Error fetching lyrics: $e', error: 'Error fetching lyrics: $e',

View File

@@ -41,7 +41,7 @@ final class LyricsFetcherProvider
} }
} }
String _$lyricsFetcherHash() => r'49468a75e00ab1533368acb52328b059831836d3'; String _$lyricsFetcherHash() => r'52296b2ccb55755ec5ad7ab751fe974dc3c64024';
abstract class _$LyricsFetcher extends $Notifier<LyricsFetcherState> { abstract class _$LyricsFetcher extends $Notifier<LyricsFetcherState> {
LyricsFetcherState build(); LyricsFetcherState build();

View File

@@ -295,7 +295,7 @@ class _PlayerLyrics extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
// Watch for track data (including lyrics) by path // Watch for track data (including lyrics) by path
final trackAsync = trackPath != null final trackAsync = trackPath != null
? ref.watch(_trackByPathProvider(trackPath!)) ? ref.watch(trackByPathProvider(trackPath!))
: const AsyncValue<db.Track?>.data(null); : const AsyncValue<db.Track?>.data(null);
final metadataAsync = trackPath != null final metadataAsync = trackPath != null
@@ -497,7 +497,7 @@ class _LyricsRefreshButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final trackAsync = ref.watch(_trackByPathProvider(trackPath)); final trackAsync = ref.watch(trackByPathProvider(trackPath));
final metadataAsync = ref.watch(trackMetadataProvider(trackPath)); final metadataAsync = ref.watch(trackMetadataProvider(trackPath));
final musixmatchProviderInstance = ref.watch(musixmatchProvider); final musixmatchProviderInstance = ref.watch(musixmatchProvider);
final neteaseProviderInstance = ref.watch(neteaseProvider); final neteaseProviderInstance = ref.watch(neteaseProvider);
@@ -522,6 +522,76 @@ class _LyricsRefreshButton extends HookConsumerWidget {
); );
} }
void _showFetchLyricsDialog(
BuildContext context,
WidgetRef ref,
db.Track track,
String trackPath,
dynamic metadataObj,
musixmatchProvider,
neteaseProvider,
) {
final metadata = metadataObj as TrackMetadata?;
final searchTerm =
'${metadata?.title ?? track.title} ${metadata?.artist ?? track.artist}'
.trim();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Fetch Lyrics'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Search term: $searchTerm'),
const SizedBox(height: 16),
Text('Choose a provider:'),
const SizedBox(height: 8),
Row(
children: [
_ProviderButton(
name: 'Musixmatch',
onPressed: () async {
Navigator.of(context).pop();
await ref
.read(lyricsFetcherProvider.notifier)
.fetchLyricsForTrack(
trackId: track.id,
searchTerm: searchTerm,
provider: musixmatchProvider,
trackPath: trackPath,
);
},
),
const SizedBox(width: 8),
_ProviderButton(
name: 'NetEase',
onPressed: () async {
Navigator.of(context).pop();
await ref
.read(lyricsFetcherProvider.notifier)
.fetchLyricsForTrack(
trackId: track.id,
searchTerm: searchTerm,
provider: neteaseProvider,
trackPath: trackPath,
);
},
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
],
),
);
}
void _showLyricsRefreshDialog( void _showLyricsRefreshDialog(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
@@ -547,21 +617,18 @@ class _LyricsRefreshButton extends HookConsumerWidget {
label: const Text('Re-fetch'), label: const Text('Re-fetch'),
onPressed: trackAsync.maybeWhen( onPressed: trackAsync.maybeWhen(
data: (track) => track != null data: (track) => track != null
? () async { ? () {
Navigator.of(context).pop(); Navigator.of(context).pop();
final metadata = metadataAsync.value; final metadata = metadataAsync.value;
final searchTerm = _showFetchLyricsDialog(
'${metadata?.title ?? track.title} ${metadata?.artist ?? track.artist}' context,
.trim(); ref,
await ref track,
.read(lyricsFetcherProvider.notifier) trackPath,
.fetchLyricsForTrack( metadata,
trackId: track.id, musixmatchProvider,
searchTerm: searchTerm, neteaseProvider,
provider: );
musixmatchProvider, // Default to Musixmatch
trackPath: trackPath,
);
} }
: null, : null,
orElse: () => null, orElse: () => null,
@@ -581,6 +648,9 @@ class _LyricsRefreshButton extends HookConsumerWidget {
data: (track) => track != null data: (track) => track != null
? () async { ? () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
debugPrint(
'Clearing lyrics for track ${track.id}',
);
final database = ref.read(databaseProvider); final database = ref.read(databaseProvider);
await (database.update( await (database.update(
database.tracks, database.tracks,
@@ -589,6 +659,12 @@ class _LyricsRefreshButton extends HookConsumerWidget {
lyrics: const drift.Value.absent(), lyrics: const drift.Value.absent(),
), ),
); );
debugPrint('Cleared lyrics from database');
// Invalidate the track provider to refresh the UI
ref.invalidate(trackByPathProvider(trackPath));
debugPrint(
'Invalidated track provider for $trackPath',
);
} }
: null, : null,
orElse: () => null, orElse: () => null,
@@ -611,7 +687,7 @@ class _LyricsRefreshButton extends HookConsumerWidget {
} }
// Provider to fetch a single track by path // Provider to fetch a single track by path
final _trackByPathProvider = FutureProvider.family<db.Track?, String>(( final trackByPathProvider = FutureProvider.family<db.Track?, String>((
ref, ref,
trackPath, trackPath,
) async { ) async {