From bf0f4bdf3ef048f2dbd3c4cff08c0495d61d3b97 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 26 Mar 2026 16:26:14 +0700 Subject: [PATCH] fix: store URL input flash on startup and FLAC metadata fallback for mismatched files Load saved registry URL before first state update to prevent brief flash of the setup screen when the store tab initializes. Add Ogg/Opus fallback in readFileMetadata when FLAC parsing fails, handling files saved with .flac extension that contain opus data. --- go_backend/exports.go | 72 ++++++++++++++------ lib/providers/store_provider.dart | 108 +++++++++++++++++++++--------- 2 files changed, 126 insertions(+), 54 deletions(-) diff --git a/go_backend/exports.go b/go_backend/exports.go index 23f80ad..830f11b 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -698,29 +698,57 @@ func ReadFileMetadata(filePath string) (string, error) { if isFlac { metadata, err := ReadMetadata(filePath) if err != nil { - return "", fmt.Errorf("failed to read metadata: %w", err) - } - result["title"] = metadata.Title - result["artist"] = metadata.Artist - result["album"] = metadata.Album - result["album_artist"] = metadata.AlbumArtist - result["date"] = metadata.Date - result["track_number"] = metadata.TrackNumber - result["disc_number"] = metadata.DiscNumber - result["isrc"] = metadata.ISRC - result["lyrics"] = metadata.Lyrics - result["genre"] = metadata.Genre - result["label"] = metadata.Label - result["copyright"] = metadata.Copyright - result["composer"] = metadata.Composer - result["comment"] = metadata.Comment + // File may have wrong extension (e.g. opus saved as .flac). + // Try Ogg/Opus parser as fallback before giving up. + GoLog("[ReadFileMetadata] FLAC parse failed for %s, trying Ogg fallback: %v\n", filePath, err) + oggMeta, oggErr := ReadOggVorbisComments(filePath) + if oggErr == nil && oggMeta != nil { + result["title"] = oggMeta.Title + result["artist"] = oggMeta.Artist + result["album"] = oggMeta.Album + result["album_artist"] = oggMeta.AlbumArtist + result["date"] = oggMeta.Date + if oggMeta.Date == "" { + result["date"] = oggMeta.Year + } + result["track_number"] = oggMeta.TrackNumber + result["disc_number"] = oggMeta.DiscNumber + result["isrc"] = oggMeta.ISRC + result["lyrics"] = oggMeta.Lyrics + result["genre"] = oggMeta.Genre + result["composer"] = oggMeta.Composer + result["comment"] = oggMeta.Comment + quality, qualityErr := GetOggQuality(filePath) + if qualityErr == nil { + result["sample_rate"] = quality.SampleRate + result["duration"] = quality.Duration + } + } else { + return "", fmt.Errorf("failed to read metadata: %w", err) + } + } else { + result["title"] = metadata.Title + result["artist"] = metadata.Artist + result["album"] = metadata.Album + result["album_artist"] = metadata.AlbumArtist + result["date"] = metadata.Date + result["track_number"] = metadata.TrackNumber + result["disc_number"] = metadata.DiscNumber + result["isrc"] = metadata.ISRC + result["lyrics"] = metadata.Lyrics + result["genre"] = metadata.Genre + result["label"] = metadata.Label + result["copyright"] = metadata.Copyright + result["composer"] = metadata.Composer + result["comment"] = metadata.Comment - quality, qualityErr := GetAudioQuality(filePath) - if qualityErr == nil { - result["bit_depth"] = quality.BitDepth - result["sample_rate"] = quality.SampleRate - if quality.SampleRate > 0 && quality.TotalSamples > 0 { - result["duration"] = int(quality.TotalSamples / int64(quality.SampleRate)) + quality, qualityErr := GetAudioQuality(filePath) + if qualityErr == nil { + result["bit_depth"] = quality.BitDepth + result["sample_rate"] = quality.SampleRate + if quality.SampleRate > 0 && quality.TotalSamples > 0 { + result["duration"] = int(quality.TotalSamples / int64(quality.SampleRate)) + } } } } else if isM4A { diff --git a/lib/providers/store_provider.dart b/lib/providers/store_provider.dart index bac55c5..fc56c01 100644 --- a/lib/providers/store_provider.dart +++ b/lib/providers/store_provider.dart @@ -12,13 +12,13 @@ const _registryUrlPrefKey = 'store_registry_url'; int compareVersions(String v1, String v2) { final parts1 = v1.replaceAll(_leadingVersionPrefix, '').split('.'); final parts2 = v2.replaceAll(_leadingVersionPrefix, '').split('.'); - + final maxLen = parts1.length > parts2.length ? parts1.length : parts2.length; - + for (var i = 0; i < maxLen; i++) { final n1 = i < parts1.length ? (int.tryParse(parts1[i]) ?? 0) : 0; final n2 = i < parts2.length ? (int.tryParse(parts2[i]) ?? 0) : 0; - + if (n1 < n2) return -1; if (n1 > n2) return 1; } @@ -26,14 +26,19 @@ int compareVersions(String v1, String v2) { } class StoreCategory { - static const String metadata = 'metadata'; static const String download = 'download'; static const String utility = 'utility'; static const String lyrics = 'lyrics'; static const String integration = 'integration'; - static const List all = [metadata, download, utility, lyrics, integration]; + static const List all = [ + metadata, + download, + utility, + lyrics, + integration, + ]; static String getDisplayName(String category) { switch (category) { @@ -94,7 +99,8 @@ class StoreExtension { return StoreExtension( id: json['id'] as String? ?? '', name: json['name'] as String? ?? '', - displayName: json['display_name'] as String? ?? json['name'] as String? ?? '', + displayName: + json['display_name'] as String? ?? json['name'] as String? ?? '', version: json['version'] as String? ?? '0.0.0', author: json['author'] as String? ?? 'Unknown', description: json['description'] as String? ?? '', @@ -117,7 +123,6 @@ class StoreExtension { } } - class StoreState { final List extensions; final String? selectedCategory; @@ -160,11 +165,15 @@ class StoreState { }) { return StoreState( extensions: extensions ?? this.extensions, - selectedCategory: clearCategory ? null : (selectedCategory ?? this.selectedCategory), + selectedCategory: clearCategory + ? null + : (selectedCategory ?? this.selectedCategory), searchQuery: searchQuery ?? this.searchQuery, isLoading: isLoading ?? this.isLoading, isDownloading: isDownloading ?? this.isDownloading, - downloadingId: clearDownloadingId ? null : (downloadingId ?? this.downloadingId), + downloadingId: clearDownloadingId + ? null + : (downloadingId ?? this.downloadingId), error: clearError ? null : (error ?? this.error), isInitialized: isInitialized ?? this.isInitialized, registryUrl: registryUrl ?? this.registryUrl, @@ -180,13 +189,16 @@ class StoreState { if (searchQuery.isNotEmpty) { final query = searchQuery.toLowerCase(); - result = result.where((e) => - e.name.toLowerCase().contains(query) || - e.displayName.toLowerCase().contains(query) || - e.description.toLowerCase().contains(query) || - e.author.toLowerCase().contains(query) || - e.tags.any((t) => t.toLowerCase().contains(query)) - ).toList(); + result = result + .where( + (e) => + e.name.toLowerCase().contains(query) || + e.displayName.toLowerCase().contains(query) || + e.description.toLowerCase().contains(query) || + e.author.toLowerCase().contains(query) || + e.tags.any((t) => t.toLowerCase().contains(query)), + ) + .toList(); } return result; @@ -206,23 +218,28 @@ class StoreNotifier extends Notifier { Future initialize(String cacheDir) async { if (state.isInitialized) return; - state = state.copyWith(isLoading: true, clearError: true); + // Load saved registry URL early to avoid UI flash (empty → setup screen) + final prefs = await SharedPreferences.getInstance(); + final savedUrl = prefs.getString(_registryUrlPrefKey) ?? ''; + + state = state.copyWith( + isLoading: true, + clearError: true, + registryUrl: savedUrl, + ); try { await PlatformBridge.initExtensionStore(cacheDir); - // Load saved registry URL from SharedPreferences - final prefs = await SharedPreferences.getInstance(); - final savedUrl = prefs.getString(_registryUrlPrefKey) ?? ''; - if (savedUrl.isNotEmpty) { await PlatformBridge.setStoreRegistryUrl(savedUrl); - state = state.copyWith(registryUrl: savedUrl); await refresh(); } state = state.copyWith(isInitialized: true, isLoading: false); - _log.i('Extension store initialized (registryUrl: ${savedUrl.isEmpty ? "not set" : savedUrl})'); + _log.i( + 'Extension store initialized (registryUrl: ${savedUrl.isEmpty ? "not set" : savedUrl})', + ); } catch (e) { _log.e('Failed to initialize store: $e'); state = state.copyWith(isLoading: false, error: e.toString()); @@ -292,7 +309,9 @@ class StoreNotifier extends Notifier { state = state.copyWith(isLoading: true, clearError: true); try { - final extensions = await PlatformBridge.getStoreExtensions(forceRefresh: forceRefresh); + final extensions = await PlatformBridge.getStoreExtensions( + forceRefresh: forceRefresh, + ); state = state.copyWith( extensions: extensions.map((e) => StoreExtension.fromJson(e)).toList(), isLoading: false, @@ -320,12 +339,23 @@ class StoreNotifier extends Notifier { state = state.copyWith(searchQuery: '', clearCategory: true); } - Future installExtension(String extensionId, String tempDir, String extensionsDir) async { - state = state.copyWith(isDownloading: true, downloadingId: extensionId, clearError: true); + Future installExtension( + String extensionId, + String tempDir, + String extensionsDir, + ) async { + state = state.copyWith( + isDownloading: true, + downloadingId: extensionId, + clearError: true, + ); try { _log.i('Downloading extension: $extensionId'); - final downloadPath = await PlatformBridge.downloadStoreExtension(extensionId, tempDir); + final downloadPath = await PlatformBridge.downloadStoreExtension( + extensionId, + tempDir, + ); _log.i('Installing extension from: $downloadPath'); final extNotifier = ref.read(extensionProvider.notifier); @@ -340,18 +370,28 @@ class StoreNotifier extends Notifier { return success; } catch (e) { _log.e('Failed to install extension: $e'); - state = state.copyWith(isDownloading: false, clearDownloadingId: true, error: e.toString()); + state = state.copyWith( + isDownloading: false, + clearDownloadingId: true, + error: e.toString(), + ); return false; } } - Future updateExtension(String extensionId, String tempDir) async { - state = state.copyWith(isDownloading: true, downloadingId: extensionId, clearError: true); + state = state.copyWith( + isDownloading: true, + downloadingId: extensionId, + clearError: true, + ); try { _log.i('Downloading update for: $extensionId'); - final downloadPath = await PlatformBridge.downloadStoreExtension(extensionId, tempDir); + final downloadPath = await PlatformBridge.downloadStoreExtension( + extensionId, + tempDir, + ); _log.i('Upgrading extension from: $downloadPath'); final extNotifier = ref.read(extensionProvider.notifier); @@ -366,7 +406,11 @@ class StoreNotifier extends Notifier { return success; } catch (e) { _log.e('Failed to update extension: $e'); - state = state.copyWith(isDownloading: false, clearDownloadingId: true, error: e.toString()); + state = state.copyWith( + isDownloading: false, + clearDownloadingId: true, + error: e.toString(), + ); return false; } }