diff --git a/lib/screens/track_metadata_cards.dart b/lib/screens/track_metadata_cards.dart index 04420be2..d2785c6c 100644 --- a/lib/screens/track_metadata_cards.dart +++ b/lib/screens/track_metadata_cards.dart @@ -440,6 +440,11 @@ extension _TrackMetadataCards on _TrackMetadataScreenState { _MetadataItem(context.l10n.trackDuration, formatClock(duration!)), if (audioQualityStr != null) _MetadataItem(context.l10n.trackAudioQuality, audioQualityStr), + if (_embeddedCoverDimensions case final dimensions?) + _MetadataItem( + context.l10n.trackCoverResolution, + '${dimensions.width} × ${dimensions.height} px', + ), if (releaseDate != null && releaseDate!.isNotEmpty) _MetadataItem(context.l10n.trackReleaseDate, releaseDate!), if (genre != null && genre!.isNotEmpty) diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 5175dc52..aac47279 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -67,10 +67,12 @@ final _log = AppLogger('TrackMetadata'); class _EmbeddedCoverPreviewCacheEntry { final String previewPath; final String? sourceValidationToken; + final ({int width, int height})? dimensions; const _EmbeddedCoverPreviewCacheEntry({ required this.previewPath, this.sourceValidationToken, + this.dimensions, }); } @@ -144,6 +146,7 @@ class _TrackMetadataScreenState extends ConsumerState Map? _editedMetadata; String? _resolvedAudioFormat; String? _embeddedCoverPreviewPath; + ({int width, int height})? _embeddedCoverDimensions; static final RegExp _invalidFileNameChars = RegExp(r'[<>:"/\\|?*\x00-\x1f]'); static final RegExp _multiUnderscore = RegExp(r'_+'); static final RegExp _leadingOrTrailingDots = RegExp(r'^\.+|\.+$'); @@ -235,28 +238,26 @@ class _TrackMetadataScreenState extends ConsumerState filePath == cleanFilePath && exists && !_hasPath(_embeddedCoverPreviewPath)) { - final cachedPath = await _getCachedEmbeddedCoverPreviewPathIfValid( + final cachedCover = await _getCachedEmbeddedCoverPreviewIfValid( _coverCacheKey, filePath, ); if (mounted && generation == _metadataLoadGeneration && filePath == cleanFilePath && - _hasPath(cachedPath)) { - setState(() => _embeddedCoverPreviewPath = cachedPath); + cachedCover != null) { + setState(() { + _embeddedCoverPreviewPath = cachedCover.previewPath; + _embeddedCoverDimensions = cachedCover.dimensions; + }); } else if (mounted && generation == _metadataLoadGeneration && filePath == cleanFilePath) { - final localCoverExists = - _hasPath(_localCoverPath) && await fileExists(_localCoverPath!); - if (!mounted || - generation != _metadataLoadGeneration || - filePath != cleanFilePath) { - return; - } - if (!localCoverExists && !_hasPath(_coverUrl)) { - unawaited(_refreshEmbeddedCoverPreview()); - } + // The information card reports the artwork embedded in the audio + // file, not a potentially resized Library thumbnail or remote cover. + // Extraction is cached, so revisiting the same track does not repeat + // the work. + unawaited(_refreshEmbeddedCoverPreview()); } } } @@ -589,6 +590,7 @@ class _TrackMetadataScreenState extends ConsumerState _editedMetadata = null; _resolvedAudioFormat = null; _embeddedCoverPreviewPath = null; + _embeddedCoverDimensions = null; }); if (scrollController.hasClients) { diff --git a/lib/screens/track_metadata_screen_cover.dart b/lib/screens/track_metadata_screen_cover.dart index f04887eb..9b4c08e8 100644 --- a/lib/screens/track_metadata_screen_cover.dart +++ b/lib/screens/track_metadata_screen_cover.dart @@ -35,6 +35,7 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { String cacheKey, String sourcePath, String previewPath, + ({int width, int height})? dimensions, ) async { final sourceValidationToken = await _readLocalFileValidationToken( sourcePath, @@ -45,6 +46,7 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { _EmbeddedCoverPreviewCacheEntry( previewPath: previewPath, sourceValidationToken: sourceValidationToken, + dimensions: dimensions, ); if (existing != null && existing.previewPath != previewPath) { await _cleanupTempFileAndParentIfNotCached(existing.previewPath); @@ -74,7 +76,8 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { } } - Future _getCachedEmbeddedCoverPreviewPathIfValid( + Future<_EmbeddedCoverPreviewCacheEntry?> + _getCachedEmbeddedCoverPreviewIfValid( String cacheKey, String sourcePath, ) async { @@ -99,7 +102,7 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { } } - return cached.previewPath; + return cached; } Future _refreshEmbeddedCoverPreview({bool force = false}) async { @@ -107,22 +110,27 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { final cacheKey = _coverCacheKey; final sourcePath = cleanFilePath; if (!force) { - final cachedPath = await _getCachedEmbeddedCoverPreviewPathIfValid( + final cachedCover = await _getCachedEmbeddedCoverPreviewIfValid( cacheKey, sourcePath, ); - if (_hasPath(cachedPath)) { + if (cachedCover != null) { if (mounted && generation == _metadataLoadGeneration && sourcePath == cleanFilePath && - _embeddedCoverPreviewPath != cachedPath) { - setState(() => _embeddedCoverPreviewPath = cachedPath); + (_embeddedCoverPreviewPath != cachedCover.previewPath || + _embeddedCoverDimensions != cachedCover.dimensions)) { + setState(() { + _embeddedCoverPreviewPath = cachedCover.previewPath; + _embeddedCoverDimensions = cachedCover.dimensions; + }); } return; } } String? newPreviewPath; + ({int width, int height})? newDimensions; try { if (!_fileExists) { await _invalidateEmbeddedCoverPreviewCacheForPath(cacheKey); @@ -130,7 +138,10 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { if (mounted && generation == _metadataLoadGeneration && sourcePath == cleanFilePath) { - setState(() => _embeddedCoverPreviewPath = null); + setState(() { + _embeddedCoverPreviewPath = null; + _embeddedCoverDimensions = null; + }); } return; } @@ -148,7 +159,13 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { ); if (result['error'] == null && await File(outputPath).exists()) { newPreviewPath = outputPath; - await _cacheEmbeddedCoverPreview(cacheKey, sourcePath, outputPath); + newDimensions = await FFmpegService.probeImageDimensions(outputPath); + await _cacheEmbeddedCoverPreview( + cacheKey, + sourcePath, + outputPath, + newDimensions, + ); } else { try { await tempDir.delete(recursive: true); @@ -166,7 +183,10 @@ extension _TrackMetadataCover on _TrackMetadataScreenState { return; } - setState(() => _embeddedCoverPreviewPath = newPreviewPath); + setState(() { + _embeddedCoverPreviewPath = newPreviewPath; + _embeddedCoverDimensions = newDimensions; + }); if (oldPreviewPath != null && oldPreviewPath != newPreviewPath) { await _cleanupTempFileAndParentIfNotCached(oldPreviewPath); }