Files
SpotiFLAC-Mobile/lib/utils/path_match_keys.dart
zarzet f511f30ad0 feat: add resolve API with SongLink fallback, fix multi-artist tags (#288), and cleanup
Resolve API (api.zarz.moe):
- Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API
- Add SongLink fallback when resolve API fails for Spotify (two-layer resilience)
- Remove dead code: page parser, XOR-obfuscated keys, legacy helpers

Multi-artist tag fix (#288):
- Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments
- Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift)
- Add PlatformBridge.rewriteSplitArtistTags() in Dart
- Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active
- Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks

Code cleanup:
- Remove unused imports, dead code, and redundant comments across Go and Dart
- Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
2026-04-01 02:49:19 +07:00

152 lines
3.8 KiB
Dart

import 'dart:io';
const _androidStoragePathAliases = <String>[
'/storage/emulated/0',
'/storage/emulated/legacy',
'/storage/self/primary',
'/sdcard',
'/mnt/sdcard',
];
/// Audio file extensions that the app commonly produces or converts between.
/// Used to generate extension-stripped match keys so that a file converted from
/// one format to another (e.g. .flac → .opus) is still recognised as the same
/// track.
const _audioExtensions = <String>[
'.flac',
'.m4a',
'.mp3',
'.opus',
'.ogg',
'.wav',
'.aac',
];
const _maxPathMatchKeyCacheSize = 6000;
final Map<String, Set<String>> _pathMatchKeyCache = <String, Set<String>>{};
String? _stripAudioExtension(String path) {
final lower = path.toLowerCase();
for (final ext in _audioExtensions) {
if (lower.endsWith(ext)) {
return path.substring(0, path.length - ext.length);
}
}
return null;
}
Set<String> buildPathMatchKeys(String? filePath) {
final raw = filePath?.trim() ?? '';
if (raw.isEmpty) return const {};
final cleaned = raw.startsWith('EXISTS:') ? raw.substring(7).trim() : raw;
if (cleaned.isEmpty) return const {};
final cached = _pathMatchKeyCache.remove(cleaned);
if (cached != null) {
_pathMatchKeyCache[cleaned] = cached;
return cached;
}
final keys = <String>{};
final visited = <String>{};
void addNormalized(String value) {
final trimmed = value.trim();
if (trimmed.isEmpty) return;
if (!visited.add(trimmed)) return;
keys.add(trimmed);
keys.add(trimmed.toLowerCase());
if (trimmed.contains('\\')) {
final slash = trimmed.replaceAll('\\', '/');
if (slash != trimmed) {
addNormalized(slash);
}
}
if (trimmed.contains('%')) {
try {
final decoded = Uri.decodeFull(trimmed);
if (decoded != trimmed) {
addNormalized(decoded);
}
} catch (_) {}
}
Uri? parsed;
try {
parsed = Uri.parse(trimmed);
} catch (_) {}
if (parsed != null && parsed.hasScheme) {
final withoutQueryOrFragment = parsed.replace(
query: null,
fragment: null,
);
final uriString = withoutQueryOrFragment.toString();
keys.add(uriString);
keys.add(uriString.toLowerCase());
if (parsed.scheme == 'file') {
try {
addNormalized(parsed.toFilePath());
} catch (_) {}
}
} else if (trimmed.startsWith('/')) {
try {
final asFileUri = Uri.file(trimmed).toString();
keys.add(asFileUri);
keys.add(asFileUri.toLowerCase());
} catch (_) {}
}
if (Platform.isAndroid) {
for (final alias in _androidEquivalentPaths(trimmed)) {
if (alias != trimmed) {
addNormalized(alias);
}
}
}
}
addNormalized(cleaned);
final extensionStrippedKeys = <String>{};
for (final key in keys) {
final stripped = _stripAudioExtension(key);
if (stripped != null && stripped.isNotEmpty) {
extensionStrippedKeys.add(stripped);
}
}
keys.addAll(extensionStrippedKeys);
final result = Set<String>.unmodifiable(keys);
_pathMatchKeyCache[cleaned] = result;
while (_pathMatchKeyCache.length > _maxPathMatchKeyCacheSize) {
_pathMatchKeyCache.remove(_pathMatchKeyCache.keys.first);
}
return result;
}
Iterable<String> _androidEquivalentPaths(String path) {
final normalized = path.replaceAll('\\', '/');
final lower = normalized.toLowerCase();
String? suffix;
for (final prefix in _androidStoragePathAliases) {
if (lower == prefix) {
suffix = '';
break;
}
final withSlash = '$prefix/';
if (lower.startsWith(withSlash)) {
suffix = normalized.substring(prefix.length);
break;
}
}
if (suffix == null) return const [];
return _androidStoragePathAliases.map((prefix) => '$prefix$suffix');
}