🐛 Fix lyrics fetch
This commit is contained in:
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user