diff --git a/lib/data/track_repository.dart b/lib/data/track_repository.dart index bb87ecb..cb64c39 100644 --- a/lib/data/track_repository.dart +++ b/lib/data/track_repository.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter_media_metadata/flutter_media_metadata.dart'; import 'package:groovybox/providers/db_provider.dart'; import 'package:path/path.dart' as p; @@ -69,7 +70,7 @@ class TrackRepository extends _$TrackRepository { mode: InsertMode.insertOrIgnore, ); } catch (e) { - print('Error importing file $path: $e'); + debugPrint('Error importing file $path: $e'); // Continue to next file } } @@ -112,7 +113,7 @@ class TrackRepository extends _$TrackRepository { try { await file.delete(); } catch (e) { - print("Error deleting file: $e"); + debugPrint("Error deleting file: $e"); } } @@ -123,7 +124,7 @@ class TrackRepository extends _$TrackRepository { try { await artFile.delete(); } catch (e) { - print("Error deleting art: $e"); + debugPrint("Error deleting art: $e"); } } } diff --git a/lib/logic/lrc_providers.dart b/lib/logic/lrc_providers.dart index 420735d..c460146 100644 --- a/lib/logic/lrc_providers.dart +++ b/lib/logic/lrc_providers.dart @@ -15,10 +15,10 @@ class Lyrics { } /// Abstract base class for LRC providers -abstract class LRCProvider { +abstract class LrcProvider { late final http.Client session; - LRCProvider() { + LrcProvider() { session = http.Client(); } @@ -29,7 +29,7 @@ abstract class LRCProvider { } /// Musixmatch LRC provider -class MusixmatchProvider extends LRCProvider { +class MusixmatchProvider extends LrcProvider { static const String rootUrl = "https://apic-desktop.musixmatch.com/ws/1.1/"; final String? lang; @@ -38,6 +38,7 @@ class MusixmatchProvider extends LRCProvider { MusixmatchProvider({this.lang, this.enhanced = false}); + @override String get name => 'Musixmatch'; Future _get( @@ -178,7 +179,7 @@ class MusixmatchProvider extends LRCProvider { } /// NetEase provider -class NetEaseProvider extends LRCProvider { +class NetEaseProvider extends LrcProvider { static const String apiEndpointMetadata = "https://music.163.com/api/search/pc"; static const String apiEndpointLyrics = @@ -211,7 +212,7 @@ class NetEaseProvider extends LRCProvider { headers: {"cookie": cookie}, ); final data = jsonDecode(response.body); - final lrc = Lyrics(plain: data["lrc"]["lyric"]); + final lrc = Lyrics(synced: data["lrc"]["lyric"]); return lrc; } diff --git a/lib/providers/lrc_fetcher_provider.dart b/lib/providers/lrc_fetcher_provider.dart index 17a36f6..386617a 100644 --- a/lib/providers/lrc_fetcher_provider.dart +++ b/lib/providers/lrc_fetcher_provider.dart @@ -1,8 +1,10 @@ +import 'package:flutter/foundation.dart'; import 'package:groovybox/data/db.dart' as db; import 'package:drift/drift.dart' as drift; import 'package:groovybox/logic/lrc_providers.dart'; import 'package:groovybox/logic/lyrics_parser.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:riverpod_annotation/riverpod_annotation.dart'; @@ -18,14 +20,18 @@ class LyricsFetcher extends _$LyricsFetcher { Future fetchLyricsForTrack({ required int trackId, required String searchTerm, - required LRCProvider provider, + required LrcProvider provider, required String trackPath, }) async { + debugPrint( + 'Fetching lyrics for track $trackId with search term: $searchTerm', + ); state = state.copyWith(isLoading: true, error: null); try { final lyrics = await provider.getLrc(searchTerm); if (lyrics == null) { + debugPrint('No lyrics found from ${provider.name}'); state = state.copyWith( isLoading: false, error: 'No lyrics found from ${provider.name}', @@ -52,17 +58,26 @@ class LyricsFetcher extends _$LyricsFetcher { ..where((t) => t.id.equals(trackId))) .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( isLoading: false, successMessage: 'Lyrics fetched from ${provider.name}', ); } else { + debugPrint('Failed to parse lyrics'); state = state.copyWith( isLoading: false, error: 'Failed to parse lyrics', ); } } catch (e) { + debugPrint('Error fetching lyrics: $e'); state = state.copyWith( isLoading: false, error: 'Error fetching lyrics: $e', diff --git a/lib/providers/lrc_fetcher_provider.g.dart b/lib/providers/lrc_fetcher_provider.g.dart index b59beb1..b2b2454 100644 --- a/lib/providers/lrc_fetcher_provider.g.dart +++ b/lib/providers/lrc_fetcher_provider.g.dart @@ -41,7 +41,7 @@ final class LyricsFetcherProvider } } -String _$lyricsFetcherHash() => r'49468a75e00ab1533368acb52328b059831836d3'; +String _$lyricsFetcherHash() => r'52296b2ccb55755ec5ad7ab751fe974dc3c64024'; abstract class _$LyricsFetcher extends $Notifier { LyricsFetcherState build(); diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 60963f8..68fa655 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -295,7 +295,7 @@ class _PlayerLyrics extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Watch for track data (including lyrics) by path final trackAsync = trackPath != null - ? ref.watch(_trackByPathProvider(trackPath!)) + ? ref.watch(trackByPathProvider(trackPath!)) : const AsyncValue.data(null); final metadataAsync = trackPath != null @@ -497,7 +497,7 @@ class _LyricsRefreshButton extends HookConsumerWidget { @override 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 musixmatchProviderInstance = ref.watch(musixmatchProvider); 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( BuildContext context, WidgetRef ref, @@ -547,21 +617,18 @@ class _LyricsRefreshButton extends HookConsumerWidget { label: const Text('Re-fetch'), onPressed: trackAsync.maybeWhen( data: (track) => track != null - ? () async { + ? () { Navigator.of(context).pop(); final metadata = metadataAsync.value; - final searchTerm = - '${metadata?.title ?? track.title} ${metadata?.artist ?? track.artist}' - .trim(); - await ref - .read(lyricsFetcherProvider.notifier) - .fetchLyricsForTrack( - trackId: track.id, - searchTerm: searchTerm, - provider: - musixmatchProvider, // Default to Musixmatch - trackPath: trackPath, - ); + _showFetchLyricsDialog( + context, + ref, + track, + trackPath, + metadata, + musixmatchProvider, + neteaseProvider, + ); } : null, orElse: () => null, @@ -581,6 +648,9 @@ class _LyricsRefreshButton extends HookConsumerWidget { data: (track) => track != null ? () async { Navigator.of(context).pop(); + debugPrint( + 'Clearing lyrics for track ${track.id}', + ); final database = ref.read(databaseProvider); await (database.update( database.tracks, @@ -589,6 +659,12 @@ class _LyricsRefreshButton extends HookConsumerWidget { 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, orElse: () => null, @@ -611,7 +687,7 @@ class _LyricsRefreshButton extends HookConsumerWidget { } // Provider to fetch a single track by path -final _trackByPathProvider = FutureProvider.family(( +final trackByPathProvider = FutureProvider.family(( ref, trackPath, ) async {