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.
This commit is contained in:
zarzet
2026-03-26 16:26:14 +07:00
parent 5e1cc3ecb5
commit bf0f4bdf3e
2 changed files with 126 additions and 54 deletions

View File

@@ -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 {

View File

@@ -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<String> all = [metadata, download, utility, lyrics, integration];
static const List<String> 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<StoreExtension> 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<StoreState> {
Future<void> 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<StoreState> {
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<StoreState> {
state = state.copyWith(searchQuery: '', clearCategory: true);
}
Future<bool> installExtension(String extensionId, String tempDir, String extensionsDir) async {
state = state.copyWith(isDownloading: true, downloadingId: extensionId, clearError: true);
Future<bool> 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<StoreState> {
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<bool> 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<StoreState> {
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;
}
}