From e820fb08dec9b94c5e8f5b4bd98b9add53707e06 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 19 Dec 2025 00:16:23 +0800 Subject: [PATCH] :recycle: Refactor lyrics fetching --- REFACTOR_SUMMARY.md | 201 ----------------------- lib/logic/lrc_providers.dart | 38 +++-- lib/providers/watch_folder_provider.dart | 8 +- lib/ui/screens/library_screen.dart | 6 +- lib/ui/screens/player_screen.dart | 9 +- lib/ui/screens/settings_screen.dart | 34 ---- pubspec.lock | 18 +- pubspec.yaml | 2 +- 8 files changed, 53 insertions(+), 263 deletions(-) delete mode 100644 REFACTOR_SUMMARY.md diff --git a/REFACTOR_SUMMARY.md b/REFACTOR_SUMMARY.md deleted file mode 100644 index fe32ed6..0000000 --- a/REFACTOR_SUMMARY.md +++ /dev/null @@ -1,201 +0,0 @@ -# GroovyBox Track Repository Refactor Summary - -## Overview -Successfully refactored the `lib/data/track_repository.dart` to support more settings including in-place indexing and folder watching functionality. - -## Key Changes Made - -### 1. Database Schema Updates (`lib/data/db.dart`) -- Added `WatchFolders` table for managing watch folders -- Added `AppSettings` table for storing app preferences -- Updated database schema version to 6 -- Added proper migrations for new tables - -### 2. Settings Provider (`lib/providers/settings_provider.dart`) -- Created comprehensive settings management with Riverpod -- Added `ImportMode` enum (Copy vs In-place) -- Added auto-scan and watch-for-changes settings -- Added supported audio formats configuration -- Persistent storage using SharedPreferences - -### 3. Watch Folder Provider (`lib/providers/watch_folder_provider.dart`) -- Created service for managing watch folders -- Added database operations for CRUD operations -- Simplified implementation avoiding complex watcher issues -- Added folder scanning functionality -- Added missing track cleanup - -### 4. Track Repository Refactor (`lib/data/track_repository.dart`) -- **Major Changes:** - - Split `importFiles` into mode-specific methods - - `_importFilesWithCopy`: Original behavior (copies files to internal storage) - - `_importFilesInPlace`: New behavior (indexes files in original location) - - Added `scanDirectory` method for folder scanning - - Added `scanWatchFolders` method for bulk scanning - - Added file event handlers (`addFileFromWatch`, `removeFileFromWatch`, `updateFileFromWatch`) - - Added `cleanupMissingTracks` for maintaining database integrity - - Updated `deleteTrack` to handle in-place vs copied files correctly - -### 5. Settings UI (`lib/ui/screens/settings_screen.dart`) -- Created comprehensive settings interface -- Import mode selection (Copy vs In-place) -- Auto-scan and watch-for-changes toggles -- Watch folders management section -- Supported formats display -- Integration with new providers - -### 6. Dependencies (`pubspec.yaml`) -- Added `watcher: ^1.2.0` for file system monitoring -- Added `shared_preferences: ^2.3.5` for settings persistence - -## New Functionality - -### Import Modes -1. **Copy Mode (Default):** - - Original behavior maintained - - Files copied to internal music directory - - Safe file management - - Suitable for mobile devices - -2. **In-place Mode:** - - Files indexed in original location - - No additional storage usage - - Preserves original file organization - - Suitable for desktop/storage-rich environments - -### Watch Folder Features -- Add/remove watch folders -- Toggle active/inactive status -- Bulk scanning of all active folders -- Automatic cleanup of missing tracks -- Support for recursive scanning - -### Settings Management -- Persistent storage of user preferences -- Auto-scan scheduling -- File change monitoring toggle -- Configurable audio formats - -## Usage Examples - -### Switch to In-place Indexing -```dart -// Update settings to use in-place indexing -ref.read(settingsProvider.notifier).setImportMode(ImportMode.inplace); -``` - -### Add Watch Folder -```dart -// Add a folder to watch list -final watchService = ref.read(watchFolderServiceProvider); -await watchService.addWatchFolder('/path/to/music', name: 'My Music'); -``` - -### Scan Watch Folders -```dart -// Scan all active watch folders -final trackRepo = ref.read(trackRepositoryProvider); -await trackRepo.scanWatchFolders(); -``` - -### Cleanup Missing Tracks -```dart -// Remove tracks that no longer exist -final trackRepo = ref.read(trackRepositoryProvider); -await trackRepo.cleanupMissingTracks(); -``` - -## Benefits - -### User Experience -- Flexible import options for different use cases -- Automatic library maintenance -- Real-time folder monitoring capabilities -- Preserved file organization when desired - -### Performance -- Efficient database operations -- Selective file scanning -- Proper resource cleanup -- Minimal storage impact for in-place mode - -### Maintainability -- Clear separation of concerns -- Modular provider architecture -- Comprehensive error handling -- Extensible design for future features - -## Future Enhancements - -### Potential Additions -1. Real-time file watching implementation -2. Advanced file format detection -3. Folder exclusion/inclusion patterns -4. Metadata caching for performance -5. Batch operations optimization -6. Conflict resolution for duplicate files - -### UI Improvements -1. Watch folder management interface -2. Import progress indicators -3. Folder scanning status -4. Settings organization and search -5. Conflict resolution dialogs - -## Migration Guide - -### For Existing Users -- Current behavior preserved (copy mode by default) -- Manual switch to in-place mode available -- Existing copied files unaffected -- Gradual migration possible - -### Recommended Workflow -1. Start with copy mode for testing -2. Add watch folders in in-place mode -3. Enable auto-scan when comfortable -4. Use cleanup to maintain library - -## Technical Notes - -### Database Considerations -- Unique path constraint ensures no duplicates -- Cascade deletion maintains referential integrity -- Proper indexing on path for performance -- Migration handles existing installations - -### File System Safety -- Existence checks before operations -- Graceful error handling -- Safe disposal of file watchers -- Album art always stored internally - -### Memory Management -- Lazy loading of watch folders -- Efficient streaming for large libraries -- Proper cleanup of resources -- Minimal memory footprint - -## Testing Recommendations - -### Unit Tests -- Test import mode switching -- Test watch folder operations -- Test file event handling -- Test cleanup functionality -- Test settings persistence - -### Integration Tests -- Test full import workflows -- Test settings changes -- Test database migrations -- Test file system scenarios - -### Edge Cases -- Large file collections -- Network storage scenarios -- Permission denials -- File system errors -- Corrupted metadata - -This refactor provides a solid foundation for enhanced music library management while maintaining backward compatibility and enabling powerful new features. diff --git a/lib/logic/lrc_providers.dart b/lib/logic/lrc_providers.dart index 2208b1d..7701073 100644 --- a/lib/logic/lrc_providers.dart +++ b/lib/logic/lrc_providers.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:dio/dio.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart' as path_provider; import 'dart:io'; @@ -16,10 +16,10 @@ class Lyrics { /// Abstract base class for LRC providers abstract class LrcProvider { - late final http.Client session; + late final Dio session; LrcProvider() { - session = http.Client(); + session = Dio(); } String get name; @@ -41,7 +41,7 @@ class MusixmatchProvider extends LrcProvider { @override String get name => 'Musixmatch'; - Future _get( + Future _get( String action, List> query, ) async { @@ -55,7 +55,7 @@ class MusixmatchProvider extends LrcProvider { final t = DateTime.now().millisecondsSinceEpoch.toString(); query.add(MapEntry("t", t)); final url = rootUrl + action; - return await session.get(Uri.parse(url), headers: Map.fromEntries(query)); + return await session.get(url, queryParameters: Map.fromEntries(query)); } Future _getToken() async { @@ -84,7 +84,7 @@ class MusixmatchProvider extends LrcProvider { await Future.delayed(Duration(seconds: 10)); return await _getToken(); } - final newToken = jsonDecode(d.body)["message"]["body"]["user_token"]; + final newToken = jsonDecode(d.data)["message"]["body"]["user_token"]; final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; final expirationTime = currentTime + 600; // 10 minutes token = newToken; @@ -105,7 +105,7 @@ class MusixmatchProvider extends LrcProvider { MapEntry("translation_fields_set", "minimal"), MapEntry("selected_language", lang!), ]); - final bodyTr = jsonDecode(rTr.body)["message"]["body"]; + final bodyTr = jsonDecode(rTr.data)["message"]["body"]; if (bodyTr["translations_list"] == null || (bodyTr["translations_list"] as List).isEmpty) { throw Exception("Couldn't find translations"); @@ -113,7 +113,7 @@ class MusixmatchProvider extends LrcProvider { // Translation handling would need full implementation } if (r.statusCode != 200) return null; - final body = jsonDecode(r.body)["message"]["body"]; + final body = jsonDecode(r.data)["message"]["body"]; if (body == null) return null; final lrcStr = body["subtitle"]["subtitle_body"]; final lrc = Lyrics(synced: lrcStr); @@ -124,9 +124,9 @@ class MusixmatchProvider extends LrcProvider { var lrc = Lyrics(); final r = await _get("track.richsync.get", [MapEntry("track_id", trackId)]); if (r.statusCode == 200 && - jsonDecode(r.body)["message"]["header"]["status_code"] == 200) { + jsonDecode(r.data)["message"]["header"]["status_code"] == 200) { final lrcRaw = jsonDecode( - r.body, + r.data, )["message"]["body"]["richsync"]["richsync_body"]; final data = jsonDecode(lrcRaw); String lrcStr = ""; @@ -157,9 +157,9 @@ class MusixmatchProvider extends LrcProvider { MapEntry("page_size", "5"), MapEntry("page", "1"), ]); - final statusCode = jsonDecode(r.body)["message"]["header"]["status_code"]; + final statusCode = jsonDecode(r.data)["message"]["header"]["status_code"]; if (statusCode != 200) return null; - final body = jsonDecode(r.body)["message"]["body"]; + final body = jsonDecode(r.data)["message"]["body"]; if (body == null || body is! Map) return null; final tracks = body["track_list"]; if (tracks == null || tracks is! List || tracks.isEmpty) return null; @@ -194,12 +194,13 @@ class NetEaseProvider extends LrcProvider { Future?> searchTrack(String searchTerm) async { final params = {"limit": "10", "type": "1", "offset": "0", "s": searchTerm}; final response = await session.get( - Uri.parse(apiEndpointMetadata).replace(queryParameters: params), - headers: {"cookie": cookie}, + apiEndpointMetadata, + queryParameters: params, + options: Options(headers: {"cookie": cookie}), ); // Update the session cookies from the new sent cookies for the next request. // In http package, we can set it, but for simplicity, pass to next call - final results = jsonDecode(response.body)["result"]["songs"]; + final results = jsonDecode(response.data)["result"]["songs"]; if (results == null || results.isEmpty) return null; // Simple best match - first track return results[0]; @@ -208,10 +209,11 @@ class NetEaseProvider extends LrcProvider { Future getLrcById(String trackId) async { final params = {"id": trackId, "lv": "1"}; final response = await session.get( - Uri.parse(apiEndpointLyrics).replace(queryParameters: params), - headers: {"cookie": cookie}, + apiEndpointLyrics, + queryParameters: params, + options: Options(headers: {"cookie": cookie}), ); - final data = jsonDecode(response.body); + final data = jsonDecode(response.data); final lrc = Lyrics(synced: data["lrc"]["lyric"]); return lrc; } diff --git a/lib/providers/watch_folder_provider.dart b/lib/providers/watch_folder_provider.dart index 0b4f4f4..6adc164 100644 --- a/lib/providers/watch_folder_provider.dart +++ b/lib/providers/watch_folder_provider.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:riverpod/riverpod.dart'; +import 'package:groovybox/data/db.dart'; +import 'package:groovybox/data/track_repository.dart'; +import 'package:groovybox/providers/db_provider.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:drift/drift.dart'; -import '../data/db.dart'; -import '../data/track_repository.dart'; -import '../providers/db_provider.dart'; // Simple watch folder provider using Riverpod final watchFoldersProvider = FutureProvider>((ref) async { diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 8b63cee..5d29a7f 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -732,7 +732,7 @@ class LibraryScreen extends HookConsumerWidget { if (await file.exists()) { final stat = await file.stat(); final sizeInMB = (stat.size / (1024 * 1024)).toStringAsFixed(2); - fileSize = '${sizeInMB} MB'; + fileSize = '$sizeInMB MB'; dateAdded = stat.modified.toString().split( ' ', )[0]; // Just the date part @@ -752,7 +752,9 @@ class LibraryScreen extends HookConsumerWidget { } }); - final screenSize = MediaQuery.of(context).size; + if (!context.mounted) return; + + final screenSize = MediaQuery.sizeOf(context); showDialog( context: context, diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 7bd220f..7763c6b 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -371,7 +371,12 @@ class _CoverView extends StatelessWidget { child: Column( children: [ Expanded( - child: Center(child: _PlayerCoverArt(metadataAsync: metadataAsync)), + child: Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Center( + child: _PlayerCoverArt(metadataAsync: metadataAsync), + ), + ), ), _PlayerControls( player: player, @@ -456,7 +461,7 @@ class _PlayerCoverArt extends StatelessWidget { loading: () => const Center(child: CircularProgressIndicator()), error: (_, _) => Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(24), ), child: Center( diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index a967b2c..5caa384 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -160,40 +160,6 @@ class SettingsScreen extends ConsumerWidget { ), ), ), - - const SizedBox(height: 16), - - // Supported Formats Section - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Supported Formats', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 4, - children: settings.supportedFormats.map((format) { - return Chip( - label: Text(format.toUpperCase()), - backgroundColor: Theme.of( - context, - ).primaryColor.withOpacity(0.1), - ); - }).toList(), - ), - ], - ), - ), - ), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index b74d797..fb1be24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -305,6 +305,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" drift: dependency: "direct main" description: @@ -505,7 +521,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: "direct main" + dependency: transitive description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/pubspec.yaml b/pubspec.yaml index 3b09bcb..0a12e9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: gap: ^3.0.1 styled_widget: ^0.4.1 super_sliver_list: ^0.4.1 - http: ^1.0.0 + dio: ^5.0.0 audio_service: ^0.18.18 palette_generator: ^0.3.3+4 watcher: ^1.2.0