mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-04 09:10:48 +02:00
feat(library): add lyrics filtering and metadata actions
This commit is contained in:
@@ -33,6 +33,8 @@ class DownloadHistoryItem {
|
||||
final String? label;
|
||||
final String? copyright;
|
||||
final bool explicit;
|
||||
final bool hasLyrics;
|
||||
final int lyricsMetadataScanVersion;
|
||||
|
||||
const DownloadHistoryItem({
|
||||
required this.id,
|
||||
@@ -67,6 +69,8 @@ class DownloadHistoryItem {
|
||||
this.label,
|
||||
this.copyright,
|
||||
this.explicit = false,
|
||||
this.hasLyrics = false,
|
||||
this.lyricsMetadataScanVersion = 0,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -102,6 +106,8 @@ class DownloadHistoryItem {
|
||||
'label': label,
|
||||
'copyright': copyright,
|
||||
'explicit': explicit,
|
||||
'hasLyrics': hasLyrics,
|
||||
'lyricsMetadataScanVersion': lyricsMetadataScanVersion,
|
||||
};
|
||||
|
||||
factory DownloadHistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -138,6 +144,9 @@ class DownloadHistoryItem {
|
||||
label: json['label'] as String?,
|
||||
copyright: json['copyright'] as String?,
|
||||
explicit: parseExplicitFlag(json['explicit']) == true,
|
||||
hasLyrics: json['hasLyrics'] == true || json['hasLyrics'] == 1,
|
||||
lyricsMetadataScanVersion:
|
||||
(json['lyricsMetadataScanVersion'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
DownloadHistoryItem copyWith({
|
||||
@@ -170,6 +179,8 @@ class DownloadHistoryItem {
|
||||
String? label,
|
||||
String? copyright,
|
||||
bool? explicit,
|
||||
bool? hasLyrics,
|
||||
int? lyricsMetadataScanVersion,
|
||||
}) {
|
||||
return DownloadHistoryItem(
|
||||
id: id,
|
||||
@@ -204,6 +215,9 @@ class DownloadHistoryItem {
|
||||
label: label ?? this.label,
|
||||
copyright: copyright ?? this.copyright,
|
||||
explicit: explicit ?? this.explicit,
|
||||
hasLyrics: hasLyrics ?? this.hasLyrics,
|
||||
lyricsMetadataScanVersion:
|
||||
lyricsMetadataScanVersion ?? this.lyricsMetadataScanVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
||||
import 'package:spotiflac_android/utils/int_utils.dart';
|
||||
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
|
||||
import 'package:spotiflac_android/utils/path_match_keys.dart';
|
||||
|
||||
part 'download_history_models.dart';
|
||||
@@ -202,6 +203,15 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
normalizeOptionalString(item.copyright) ??
|
||||
normalizeOptionalString(existing.copyright),
|
||||
explicit: item.explicit || existing.explicit,
|
||||
hasLyrics:
|
||||
item.lyricsMetadataScanVersion >=
|
||||
existing.lyricsMetadataScanVersion
|
||||
? item.hasLyrics
|
||||
: existing.hasLyrics,
|
||||
lyricsMetadataScanVersion: max(
|
||||
item.lyricsMetadataScanVersion,
|
||||
existing.lyricsMetadataScanVersion,
|
||||
),
|
||||
);
|
||||
return (item: mergedItem, existingId: existing?.id);
|
||||
}
|
||||
@@ -479,6 +489,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
int? duration,
|
||||
String? composer,
|
||||
bool? explicit,
|
||||
bool? hasLyrics,
|
||||
int? lyricsMetadataScanVersion,
|
||||
}) async {
|
||||
final target = await _historyItemForUpdate(id);
|
||||
if (target == null) {
|
||||
@@ -502,6 +514,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
duration: duration,
|
||||
composer: composer,
|
||||
explicit: explicit,
|
||||
hasLyrics: hasLyrics,
|
||||
lyricsMetadataScanVersion: lyricsMetadataScanVersion,
|
||||
);
|
||||
|
||||
if (updated.quality == current.quality &&
|
||||
@@ -515,7 +529,10 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
updated.totalDiscs == current.totalDiscs &&
|
||||
updated.duration == current.duration &&
|
||||
updated.composer == current.composer &&
|
||||
updated.explicit == current.explicit) {
|
||||
updated.explicit == current.explicit &&
|
||||
updated.hasLyrics == current.hasLyrics &&
|
||||
updated.lyricsMetadataScanVersion ==
|
||||
current.lyricsMetadataScanVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,6 +567,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
String? label,
|
||||
String? copyright,
|
||||
bool? explicit,
|
||||
bool? hasLyrics,
|
||||
int? lyricsMetadataScanVersion,
|
||||
}) async {
|
||||
final target = await _historyItemForUpdate(id);
|
||||
if (target == null) {
|
||||
@@ -574,6 +593,8 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
label: label,
|
||||
copyright: copyright,
|
||||
explicit: explicit,
|
||||
hasLyrics: hasLyrics,
|
||||
lyricsMetadataScanVersion: lyricsMetadataScanVersion,
|
||||
);
|
||||
|
||||
final updatedItems = target.index >= 0
|
||||
|
||||
@@ -260,11 +260,19 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
trimmed.endsWith('.aac') ||
|
||||
trimmed.endsWith('.mp3') ||
|
||||
trimmed.endsWith('.opus') ||
|
||||
trimmed.endsWith('.ogg');
|
||||
trimmed.endsWith('.ogg') ||
|
||||
trimmed.endsWith('.ape') ||
|
||||
trimmed.endsWith('.wv') ||
|
||||
trimmed.endsWith('.mpc') ||
|
||||
trimmed.endsWith('.wav') ||
|
||||
trimmed.endsWith('.aiff') ||
|
||||
trimmed.endsWith('.aif') ||
|
||||
trimmed.endsWith('.aifc');
|
||||
}
|
||||
|
||||
bool _shouldBackfillAudioMetadata(DownloadHistoryItem item) {
|
||||
return _needsAverageBitrateBackfill(item) ||
|
||||
return item.lyricsMetadataScanVersion < 1 ||
|
||||
_needsAverageBitrateBackfill(item) ||
|
||||
_shouldBackfillAudioMetadataIgnoringBitrate(item);
|
||||
}
|
||||
|
||||
@@ -372,10 +380,54 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool?> _probeSidecarLyrics(DownloadHistoryItem item) async {
|
||||
final filePath = item.filePath.trim();
|
||||
if (filePath.isEmpty) return null;
|
||||
|
||||
String? tempPath;
|
||||
try {
|
||||
if (filePath.startsWith('content://')) {
|
||||
final treeUri = normalizeOptionalString(item.downloadTreeUri);
|
||||
final fileName = normalizeOptionalString(item.safFileName);
|
||||
if (treeUri == null || fileName == null) return null;
|
||||
final replacedName = fileName.replaceFirst(RegExp(r'\.[^.]+$'), '.lrc');
|
||||
final lrcName = replacedName == fileName
|
||||
? '$fileName.lrc'
|
||||
: replacedName;
|
||||
final resolved = await PlatformBridge.resolveSafFile(
|
||||
treeUri: treeUri,
|
||||
relativeDir: item.safRelativeDir ?? '',
|
||||
fileName: lrcName,
|
||||
);
|
||||
final uri = normalizeOptionalString(resolved['uri']?.toString());
|
||||
if (uri == null) return false;
|
||||
tempPath = await PlatformBridge.copyContentUriToTemp(uri);
|
||||
if (tempPath == null) return null;
|
||||
return hasUsableLyricsContent(await File(tempPath).readAsString());
|
||||
}
|
||||
|
||||
final lrcPath = filePath.replaceAll(RegExp(r'\.[^.]+$'), '.lrc');
|
||||
final safeLrcPath = lrcPath == filePath ? '$filePath.lrc' : lrcPath;
|
||||
final sidecar = File(safeLrcPath);
|
||||
if (!await sidecar.exists()) return false;
|
||||
return hasUsableLyricsContent(await sidecar.readAsString());
|
||||
} catch (e) {
|
||||
_historyLog.d('Sidecar lyrics probe failed for $filePath: $e');
|
||||
return null;
|
||||
} finally {
|
||||
if (tempPath != null) {
|
||||
try {
|
||||
await File(tempPath).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _probeAudioMetadata(
|
||||
String filePath, {
|
||||
DownloadHistoryItem item, {
|
||||
String? fallbackQuality,
|
||||
}) async {
|
||||
final filePath = item.filePath;
|
||||
if (!_supportsAudioMetadataProbe(filePath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -414,6 +466,13 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
final totalTracks = readPositiveInt(result['total_tracks']);
|
||||
final discNumber = readPositiveInt(result['disc_number']);
|
||||
final totalDiscs = readPositiveInt(result['total_discs']);
|
||||
final embeddedHasLyrics =
|
||||
result['hasLyrics'] == true ||
|
||||
hasUsableLyricsContent(result['lyrics']?.toString() ?? '');
|
||||
final sidecarHasLyrics = await _probeSidecarLyrics(item);
|
||||
final hasLyrics = embeddedHasLyrics || sidecarHasLyrics == true;
|
||||
final lyricsMetadataScanVersion =
|
||||
embeddedHasLyrics || sidecarHasLyrics != null ? 1 : 0;
|
||||
|
||||
if (quality == null &&
|
||||
bitDepth == null &&
|
||||
@@ -425,7 +484,10 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
trackNumber == null &&
|
||||
totalTracks == null &&
|
||||
discNumber == null &&
|
||||
totalDiscs == null) {
|
||||
totalDiscs == null &&
|
||||
result['hasLyrics'] == null &&
|
||||
result['lyrics'] == null &&
|
||||
sidecarHasLyrics == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -442,6 +504,8 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
'totalTracks': totalTracks,
|
||||
'discNumber': discNumber,
|
||||
'totalDiscs': totalDiscs,
|
||||
'hasLyrics': hasLyrics,
|
||||
'lyricsMetadataScanVersion': lyricsMetadataScanVersion,
|
||||
};
|
||||
} catch (e) {
|
||||
_historyLog.d('Audio metadata probe failed for $filePath: $e');
|
||||
@@ -497,9 +561,10 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
final item = items[index];
|
||||
|
||||
Map<String, dynamic>? probed;
|
||||
if (_shouldBackfillAudioMetadataIgnoringBitrate(item)) {
|
||||
if (item.lyricsMetadataScanVersion < 1 ||
|
||||
_shouldBackfillAudioMetadataIgnoringBitrate(item)) {
|
||||
probed = await _probeAudioMetadata(
|
||||
item.filePath,
|
||||
item,
|
||||
fallbackQuality: item.quality,
|
||||
);
|
||||
} else if (_needsAverageBitrateBackfill(item)) {
|
||||
@@ -541,6 +606,9 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
final resolvedTotalTracks = probed['totalTracks'] as int?;
|
||||
final resolvedDiscNumber = probed['discNumber'] as int?;
|
||||
final resolvedTotalDiscs = probed['totalDiscs'] as int?;
|
||||
final resolvedHasLyrics = probed['hasLyrics'] as bool?;
|
||||
final resolvedLyricsScanVersion =
|
||||
probed['lyricsMetadataScanVersion'] as int?;
|
||||
|
||||
final qualityChanged =
|
||||
resolvedQuality != null && resolvedQuality != item.quality;
|
||||
@@ -566,6 +634,11 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
resolvedDiscNumber != null && resolvedDiscNumber != item.discNumber;
|
||||
final totalDiscsChanged =
|
||||
resolvedTotalDiscs != null && resolvedTotalDiscs != item.totalDiscs;
|
||||
final hasLyricsChanged =
|
||||
resolvedHasLyrics != null && resolvedHasLyrics != item.hasLyrics;
|
||||
final lyricsScanVersionChanged =
|
||||
resolvedLyricsScanVersion != null &&
|
||||
resolvedLyricsScanVersion != item.lyricsMetadataScanVersion;
|
||||
|
||||
if (!qualityChanged &&
|
||||
!bitDepthChanged &&
|
||||
@@ -577,7 +650,9 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
!trackNumberChanged &&
|
||||
!totalTracksChanged &&
|
||||
!discNumberChanged &&
|
||||
!totalDiscsChanged) {
|
||||
!totalDiscsChanged &&
|
||||
!hasLyricsChanged &&
|
||||
!lyricsScanVersionChanged) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -593,6 +668,8 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
|
||||
totalTracks: resolvedTotalTracks,
|
||||
discNumber: resolvedDiscNumber,
|
||||
totalDiscs: resolvedTotalDiscs,
|
||||
hasLyrics: resolvedHasLyrics,
|
||||
lyricsMetadataScanVersion: resolvedLyricsScanVersion,
|
||||
);
|
||||
updatedItems ??= [...items];
|
||||
updatedItems[index] = updated;
|
||||
|
||||
@@ -31,6 +31,7 @@ import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
||||
import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
|
||||
import 'package:spotiflac_android/utils/int_utils.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
|
||||
import 'package:spotiflac_android/utils/progress_stream_poller.dart';
|
||||
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
|
||||
@@ -750,10 +750,11 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
filePath: '',
|
||||
durationMs: track.duration * 1000,
|
||||
);
|
||||
if (fetchedLrc.isNotEmpty && fetchedLrc != '[instrumental:true]') {
|
||||
if (hasUsableLyricsContent(fetchedLrc) &&
|
||||
!isInstrumentalLyricsMarker(fetchedLrc)) {
|
||||
lrcContent = fetchedLrc;
|
||||
_log.d('Lyrics fetched for $format (${fetchedLrc.length} chars)');
|
||||
} else if (fetchedLrc == '[instrumental:true]') {
|
||||
} else if (isInstrumentalLyricsMarker(fetchedLrc)) {
|
||||
_log.d('Track is instrumental, skipping lyrics handling');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -259,6 +259,8 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
String? genre,
|
||||
String? label,
|
||||
String? copyright,
|
||||
required bool hasLyrics,
|
||||
required int lyricsMetadataScanVersion,
|
||||
}) {
|
||||
final backendTitle = result['title'] as String?;
|
||||
final backendArtist = result['artist'] as String?;
|
||||
@@ -343,6 +345,53 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
explicit:
|
||||
trackToDownload.isExplicit ||
|
||||
parseExplicitFlag(result['explicit']) == true,
|
||||
hasLyrics: hasLyrics,
|
||||
lyricsMetadataScanVersion: lyricsMetadataScanVersion,
|
||||
);
|
||||
}
|
||||
|
||||
Future<({bool hasLyrics, int scanVersion})> _resolveFinalLyricsAvailability({
|
||||
required String filePath,
|
||||
Map<String, dynamic>? probedMetadata,
|
||||
bool externalLrcWritten = false,
|
||||
}) async {
|
||||
var metadataScanned = false;
|
||||
var hasEmbeddedLyrics = false;
|
||||
try {
|
||||
final metadata =
|
||||
probedMetadata ?? await PlatformBridge.readFileMetadata(filePath);
|
||||
if (metadata['error'] == null &&
|
||||
(metadata.containsKey('lyrics') ||
|
||||
metadata.containsKey('hasLyrics'))) {
|
||||
metadataScanned = true;
|
||||
hasEmbeddedLyrics =
|
||||
metadata['hasLyrics'] == true ||
|
||||
hasUsableLyricsContent(metadata['lyrics']?.toString() ?? '');
|
||||
}
|
||||
} catch (e) {
|
||||
_log.d('Final lyrics metadata probe failed for $filePath: $e');
|
||||
}
|
||||
|
||||
var hasSidecarLyrics = externalLrcWritten;
|
||||
if (!isContentUri(filePath)) {
|
||||
try {
|
||||
final lrcPath = filePath.replaceAll(RegExp(r'\.[^.]+$'), '.lrc');
|
||||
final safeLrcPath = lrcPath == filePath ? '$filePath.lrc' : lrcPath;
|
||||
final sidecar = File(safeLrcPath);
|
||||
if (await sidecar.exists()) {
|
||||
hasSidecarLyrics = hasUsableLyricsContent(
|
||||
await sidecar.readAsString(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.d('Final sidecar lyrics probe failed for $filePath: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final hasLyrics = hasEmbeddedLyrics || hasSidecarLyrics;
|
||||
return (
|
||||
hasLyrics: hasLyrics,
|
||||
scanVersion: hasLyrics || metadataScanned ? 1 : 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -462,14 +511,14 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeLrcToSaf({
|
||||
Future<bool> _writeLrcToSaf({
|
||||
required String treeUri,
|
||||
required String relativeDir,
|
||||
required String baseName,
|
||||
required String lrcContent,
|
||||
}) async {
|
||||
try {
|
||||
if (lrcContent.isEmpty) return;
|
||||
if (!hasUsableLyricsContent(lrcContent)) return false;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempPath = '${tempDir.path}/$baseName.lrc';
|
||||
await File(tempPath).writeAsString(lrcContent);
|
||||
@@ -489,8 +538,10 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
try {
|
||||
await File(tempPath).delete();
|
||||
} catch (_) {}
|
||||
return uri != null;
|
||||
} catch (e) {
|
||||
_log.w('Failed to create external LRC in SAF: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1230,7 +1281,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
/// modes). [resolveBaseName] and [onFetchError] are each caller's own
|
||||
/// base-name fallback chain and fetch-failure log line, evaluated lazily
|
||||
/// to match the original call sites exactly.
|
||||
Future<void> _saveExternalLrc({
|
||||
Future<bool> _saveExternalLrc({
|
||||
required Map<String, dynamic> result,
|
||||
required AppSettings settings,
|
||||
required ExtensionState extensionState,
|
||||
@@ -1250,11 +1301,11 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
!_shouldSkipLyrics(extensionState, track.source, service) &&
|
||||
(lyricsMode == 'external' || lyricsMode == 'both');
|
||||
if (!shouldSaveExternalLrc) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
String? lrcContent = result['lyrics_lrc'] as String?;
|
||||
if (lrcContent == null || lrcContent.isEmpty) {
|
||||
if (!hasUsableLyricsContent(lrcContent ?? '')) {
|
||||
try {
|
||||
lrcContent = await PlatformBridge.getLyricsLRC(
|
||||
track.id,
|
||||
@@ -1266,31 +1317,37 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
onFetchError(e);
|
||||
}
|
||||
}
|
||||
if (lrcContent == null || lrcContent.isEmpty) {
|
||||
return;
|
||||
if (!hasUsableLyricsContent(lrcContent ?? '') ||
|
||||
isInstrumentalLyricsMarker(lrcContent!)) {
|
||||
return false;
|
||||
}
|
||||
final resolvedLrc = lrcContent;
|
||||
|
||||
if (storageMode == 'saf' && isContentUri(filePath)) {
|
||||
if (downloadTreeUri == null || downloadTreeUri.isEmpty) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
final baseName = await resolveBaseName();
|
||||
await _writeLrcToSaf(
|
||||
final written = await _writeLrcToSaf(
|
||||
treeUri: downloadTreeUri,
|
||||
relativeDir: safRelativeDir,
|
||||
baseName: baseName,
|
||||
lrcContent: lrcContent,
|
||||
lrcContent: resolvedLrc,
|
||||
);
|
||||
return;
|
||||
if (written) result['lyrics_lrc'] = resolvedLrc;
|
||||
return written;
|
||||
}
|
||||
|
||||
try {
|
||||
final lrcPath = filePath.replaceAll(RegExp(r'\.[^.]+$'), '.lrc');
|
||||
final safeLrcPath = lrcPath == filePath ? '$filePath.lrc' : lrcPath;
|
||||
await File(safeLrcPath).writeAsString(lrcContent);
|
||||
await File(safeLrcPath).writeAsString(resolvedLrc);
|
||||
result['lyrics_lrc'] = resolvedLrc;
|
||||
_log.d('Native-worker external LRC saved: $safeLrcPath');
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.w('Failed to save native-worker external LRC: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,9 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
storedQuality;
|
||||
final useSaf = context.storageMode == 'saf';
|
||||
final resultFileName = result['file_name']?.toString().trim();
|
||||
final lyricsAvailability = await _resolveFinalLyricsAvailability(
|
||||
filePath: filePath,
|
||||
);
|
||||
|
||||
await ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
@@ -172,6 +175,8 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
genre: normalizeOptionalString(result['genre']?.toString()),
|
||||
label: normalizeOptionalString(result['label']?.toString()),
|
||||
copyright: normalizeOptionalString(result['copyright']?.toString()),
|
||||
hasLyrics: lyricsAvailability.hasLyrics,
|
||||
lyricsMetadataScanVersion: lyricsAvailability.scanVersion,
|
||||
),
|
||||
preserveTrackVariant: context.item.preserveQualityVariant,
|
||||
);
|
||||
@@ -1441,7 +1446,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
if (finalQuality != null) actualQuality = finalQuality;
|
||||
}
|
||||
|
||||
await _saveExternalLrc(
|
||||
final externalLrcWritten = await _saveExternalLrc(
|
||||
result: result,
|
||||
settings: settings,
|
||||
extensionState: ref.read(extensionProvider),
|
||||
@@ -1483,6 +1488,10 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
lowerFilePath.endsWith('.ogg');
|
||||
|
||||
final completedFilePath = filePath;
|
||||
final lyricsAvailability = await _resolveFinalLyricsAvailability(
|
||||
filePath: completedFilePath,
|
||||
externalLrcWritten: externalLrcWritten,
|
||||
);
|
||||
await persistBeforePublishingDownloadCompletion(
|
||||
persist: () async {
|
||||
if (!settings.saveDownloadHistory) return;
|
||||
@@ -1511,6 +1520,8 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
copyright: normalizeOptionalString(
|
||||
result['copyright'] as String?,
|
||||
),
|
||||
hasLyrics: lyricsAvailability.hasLyrics,
|
||||
lyricsMetadataScanVersion: lyricsAvailability.scanVersion,
|
||||
),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
);
|
||||
|
||||
@@ -128,6 +128,7 @@ class _DownloadRun {
|
||||
/// Filled by the SAF embed op from the local temp so the final quality
|
||||
/// probe doesn't have to copy the published file back out of SAF.
|
||||
Map<String, dynamic>? probedFinalMetadata;
|
||||
bool externalLrcWritten = false;
|
||||
|
||||
Future<void> _run() async {
|
||||
final normalizedService = n._normalizeQueuedService(item.service);
|
||||
@@ -802,7 +803,7 @@ class _DownloadRun {
|
||||
|
||||
final lrcTarget = filePath;
|
||||
if (effectiveSafMode && lrcTarget != null && isContentUri(lrcTarget)) {
|
||||
await n._saveExternalLrc(
|
||||
externalLrcWritten = await n._saveExternalLrc(
|
||||
result: result,
|
||||
settings: settings,
|
||||
extensionState: extensionState,
|
||||
@@ -1609,7 +1610,7 @@ class _DownloadRun {
|
||||
required String format,
|
||||
bool writeExternalLrc = true,
|
||||
bool rebuildTrack = true,
|
||||
}) {
|
||||
}) async {
|
||||
final track = rebuildTrack
|
||||
? n._buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
@@ -1617,7 +1618,7 @@ class _DownloadRun {
|
||||
resolvedAlbumArtist,
|
||||
)
|
||||
: trackToDownload;
|
||||
return n._embedMetadataToFile(
|
||||
final lrcContent = await n._embedMetadataToFile(
|
||||
path,
|
||||
track,
|
||||
format: format,
|
||||
@@ -1628,6 +1629,10 @@ class _DownloadRun {
|
||||
downloadService: item.service,
|
||||
writeExternalLrc: writeExternalLrc,
|
||||
);
|
||||
if (lrcContent != null && lrcContent.isNotEmpty) {
|
||||
result['lyrics_lrc'] = lrcContent;
|
||||
}
|
||||
return lrcContent;
|
||||
}
|
||||
|
||||
Future<void> _recoverSafUriIfNeeded() async {
|
||||
@@ -1735,6 +1740,7 @@ class _DownloadRun {
|
||||
? probed
|
||||
: await PlatformBridge.readFileMetadata(path);
|
||||
if (metadata['error'] == null) {
|
||||
probedFinalMetadata = metadata;
|
||||
final probedBitDepth = metadata['bit_depth'] is num
|
||||
? (metadata['bit_depth'] as num).toInt()
|
||||
: int.tryParse(metadata['bit_depth']?.toString() ?? '');
|
||||
@@ -1790,6 +1796,11 @@ class _DownloadRun {
|
||||
final historyBitDepth = isLossyOutput ? null : finalBitDepth;
|
||||
final historySampleRate = isLossyOutput ? null : finalSampleRate;
|
||||
final historyBitrate = finalBitrateKbps;
|
||||
final lyricsAvailability = await n._resolveFinalLyricsAvailability(
|
||||
filePath: historyFilePath,
|
||||
probedMetadata: probedFinalMetadata,
|
||||
externalLrcWritten: externalLrcWritten,
|
||||
);
|
||||
|
||||
await persistBeforePublishingDownloadCompletion(
|
||||
persist: () async {
|
||||
@@ -1814,6 +1825,8 @@ class _DownloadRun {
|
||||
genre: effectiveGenre,
|
||||
label: effectiveLabel,
|
||||
copyright: effectiveCopyright,
|
||||
hasLyrics: lyricsAvailability.hasLyrics,
|
||||
lyricsMetadataScanVersion: lyricsAvailability.scanVersion,
|
||||
),
|
||||
preserveTrackVariant: item.preserveQualityVariant,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user