mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-14 23:20:25 +02:00
feat: add artist tag mode setting with split Vorbis support and improve library scan progress
- Add artist_tag_mode setting (joined / split_vorbis) for FLAC/Opus multi-artist tags - Split 'Artist A, Artist B' into separate ARTIST= Vorbis comments when split mode is enabled - Join repeated ARTIST/ALBUMARTIST Vorbis comments when reading metadata - Propagate artistTagMode through download pipeline, re-enrich, and metadata editor - Improve library scan progress: separate polling intervals, finalizing state, indeterminate progress - Add initial progress snapshot on library scan stream connect - Use req.ArtistName consistently for Qobuz downloads instead of track.Performer.Name - Add l10n keys for artist tag mode, library files unit, and scan finalizing status
This commit is contained in:
@@ -11,6 +11,7 @@ class DownloadRequestPayload {
|
||||
final String filenameFormat;
|
||||
final String quality;
|
||||
final bool embedMetadata;
|
||||
final String artistTagMode;
|
||||
final bool embedLyrics;
|
||||
final bool embedMaxQualityCover;
|
||||
final int trackNumber;
|
||||
@@ -49,6 +50,7 @@ class DownloadRequestPayload {
|
||||
required this.filenameFormat,
|
||||
this.quality = 'LOSSLESS',
|
||||
this.embedMetadata = true,
|
||||
this.artistTagMode = 'joined',
|
||||
this.embedLyrics = true,
|
||||
this.embedMaxQualityCover = true,
|
||||
this.trackNumber = 1,
|
||||
@@ -89,6 +91,7 @@ class DownloadRequestPayload {
|
||||
'filename_format': filenameFormat,
|
||||
'quality': quality,
|
||||
'embed_metadata': embedMetadata,
|
||||
'artist_tag_mode': artistTagMode,
|
||||
'embed_lyrics': embedLyrics,
|
||||
'embed_max_quality_cover': embedMaxQualityCover,
|
||||
'track_number': trackNumber,
|
||||
@@ -133,6 +136,7 @@ class DownloadRequestPayload {
|
||||
filenameFormat: filenameFormat,
|
||||
quality: quality,
|
||||
embedMetadata: embedMetadata,
|
||||
artistTagMode: artistTagMode,
|
||||
embedLyrics: embedLyrics,
|
||||
embedMaxQualityCover: embedMaxQualityCover,
|
||||
trackNumber: trackNumber,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:ffmpeg_kit_flutter_new_full/ffmpeg_session.dart';
|
||||
import 'package:ffmpeg_kit_flutter_new_full/return_code.dart';
|
||||
import 'package:ffmpeg_kit_flutter_new_full/session_state.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('FFmpeg');
|
||||
@@ -887,6 +888,7 @@ class FFmpegService {
|
||||
required String flacPath,
|
||||
String? coverPath,
|
||||
Map<String, String>? metadata,
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
}) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempOutput = _nextTempEmbedPath(tempDir.path, '.flac');
|
||||
@@ -911,10 +913,11 @@ class FFmpegService {
|
||||
cmdBuffer.write('-c:a copy ');
|
||||
|
||||
if (metadata != null) {
|
||||
metadata.forEach((key, value) {
|
||||
final sanitizedValue = value.replaceAll('"', '\\"');
|
||||
cmdBuffer.write('-metadata $key="$sanitizedValue" ');
|
||||
});
|
||||
_appendVorbisMetadataToCommandBuffer(
|
||||
cmdBuffer,
|
||||
metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
}
|
||||
|
||||
cmdBuffer.write('"$tempOutput" -y');
|
||||
@@ -1046,6 +1049,7 @@ class FFmpegService {
|
||||
required String opusPath,
|
||||
String? coverPath,
|
||||
Map<String, String>? metadata,
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
}) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempOutput = _nextTempEmbedPath(tempDir.path, '.opus');
|
||||
@@ -1063,11 +1067,11 @@ class FFmpegService {
|
||||
];
|
||||
|
||||
if (metadata != null) {
|
||||
metadata.forEach((key, value) {
|
||||
arguments
|
||||
..add('-metadata')
|
||||
..add('$key=$value');
|
||||
});
|
||||
_appendVorbisMetadataToArguments(
|
||||
arguments,
|
||||
metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
}
|
||||
|
||||
if (coverPath != null) {
|
||||
@@ -1326,6 +1330,7 @@ class FFmpegService {
|
||||
required String bitrate,
|
||||
required Map<String, String> metadata,
|
||||
String? coverPath,
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
bool deleteOriginal = true,
|
||||
}) async {
|
||||
final format = targetFormat.toLowerCase();
|
||||
@@ -1348,6 +1353,7 @@ class FFmpegService {
|
||||
inputPath: inputPath,
|
||||
metadata: metadata,
|
||||
coverPath: coverPath,
|
||||
artistTagMode: artistTagMode,
|
||||
deleteOriginal: deleteOriginal,
|
||||
);
|
||||
}
|
||||
@@ -1391,6 +1397,7 @@ class FFmpegService {
|
||||
opusPath: outputPath,
|
||||
coverPath: coverPath,
|
||||
metadata: metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1491,6 +1498,7 @@ class FFmpegService {
|
||||
required String inputPath,
|
||||
required Map<String, String> metadata,
|
||||
String? coverPath,
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
bool deleteOriginal = true,
|
||||
}) async {
|
||||
final outputPath = _buildOutputPath(inputPath, '.flac');
|
||||
@@ -1515,11 +1523,11 @@ class FFmpegService {
|
||||
cmdBuffer.write('-c:a flac -compression_level 8 ');
|
||||
cmdBuffer.write('-map_metadata 0 ');
|
||||
|
||||
final vorbisComments = _normalizeToVorbisComments(metadata);
|
||||
for (final entry in vorbisComments.entries) {
|
||||
final sanitized = entry.value.replaceAll('"', '\\"');
|
||||
cmdBuffer.write('-metadata ${entry.key}="$sanitized" ');
|
||||
}
|
||||
_appendVorbisMetadataToCommandBuffer(
|
||||
cmdBuffer,
|
||||
metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
|
||||
cmdBuffer.write('"$outputPath" -y');
|
||||
|
||||
@@ -1617,6 +1625,86 @@ class FFmpegService {
|
||||
return vorbis;
|
||||
}
|
||||
|
||||
static void _appendVorbisMetadataToCommandBuffer(
|
||||
StringBuffer cmdBuffer,
|
||||
Map<String, String> metadata, {
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
}) {
|
||||
for (final entry in _buildVorbisMetadataEntries(
|
||||
metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
)) {
|
||||
final sanitized = entry.value.replaceAll('"', '\\"');
|
||||
cmdBuffer.write('-metadata ${entry.key}="$sanitized" ');
|
||||
}
|
||||
}
|
||||
|
||||
static void _appendVorbisMetadataToArguments(
|
||||
List<String> arguments,
|
||||
Map<String, String> metadata, {
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
}) {
|
||||
for (final entry in _buildVorbisMetadataEntries(
|
||||
metadata,
|
||||
artistTagMode: artistTagMode,
|
||||
)) {
|
||||
arguments
|
||||
..add('-metadata')
|
||||
..add('${entry.key}=${entry.value}');
|
||||
}
|
||||
}
|
||||
|
||||
static List<MapEntry<String, String>> _buildVorbisMetadataEntries(
|
||||
Map<String, String> metadata, {
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
}) {
|
||||
final vorbis = _normalizeToVorbisComments(metadata);
|
||||
final entries = <MapEntry<String, String>>[];
|
||||
|
||||
for (final entry in vorbis.entries) {
|
||||
if (entry.key == 'ARTIST' || entry.key == 'ALBUMARTIST') {
|
||||
continue;
|
||||
}
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
_appendVorbisArtistEntries(
|
||||
entries,
|
||||
'ARTIST',
|
||||
vorbis['ARTIST'],
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
_appendVorbisArtistEntries(
|
||||
entries,
|
||||
'ALBUMARTIST',
|
||||
vorbis['ALBUMARTIST'],
|
||||
artistTagMode: artistTagMode,
|
||||
);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static void _appendVorbisArtistEntries(
|
||||
List<MapEntry<String, String>> entries,
|
||||
String key,
|
||||
String? rawValue, {
|
||||
String artistTagMode = artistTagModeJoined,
|
||||
}) {
|
||||
final value = rawValue?.trim() ?? '';
|
||||
if (value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldSplitVorbisArtistTags(artistTagMode)) {
|
||||
entries.add(MapEntry(key, value));
|
||||
return;
|
||||
}
|
||||
|
||||
for (final artist in splitArtistTagValues(value)) {
|
||||
entries.add(MapEntry(key, artist));
|
||||
}
|
||||
}
|
||||
|
||||
/// Map Vorbis comment keys to M4A/MP4 metadata tag names for FFmpeg.
|
||||
static Map<String, String> _convertToM4aTags(Map<String, String> metadata) {
|
||||
final m4aMap = <String, String>{};
|
||||
|
||||
Reference in New Issue
Block a user