mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-07 05:07:59 +02:00
v2.2.0: Default to Tidal, faster ISRC matching, ISRC enrichment for search
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
/// App version and info constants
|
||||
/// Update version here only - all other files will reference this
|
||||
class AppInfo {
|
||||
static const String version = '2.1.7';
|
||||
static const String buildNumber = '45';
|
||||
static const String version = '2.2.0';
|
||||
static const String buildNumber = '46';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class AppSettings {
|
||||
final String metadataSource; // spotify, deezer - source for search and metadata
|
||||
|
||||
const AppSettings({
|
||||
this.defaultService = 'qobuz',
|
||||
this.defaultService = 'tidal',
|
||||
this.audioQuality = 'LOSSLESS',
|
||||
this.filenameFormat = '{title} - {artist}',
|
||||
this.downloadDirectory = '',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -35,6 +36,9 @@ class DownloadHistoryItem {
|
||||
final int? duration;
|
||||
final String? releaseDate;
|
||||
final String? quality;
|
||||
// Audio quality info (from file after download)
|
||||
final int? bitDepth;
|
||||
final int? sampleRate;
|
||||
|
||||
const DownloadHistoryItem({
|
||||
required this.id,
|
||||
@@ -53,6 +57,8 @@ class DownloadHistoryItem {
|
||||
this.duration,
|
||||
this.releaseDate,
|
||||
this.quality,
|
||||
this.bitDepth,
|
||||
this.sampleRate,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -72,6 +78,8 @@ class DownloadHistoryItem {
|
||||
'duration': duration,
|
||||
'releaseDate': releaseDate,
|
||||
'quality': quality,
|
||||
'bitDepth': bitDepth,
|
||||
'sampleRate': sampleRate,
|
||||
};
|
||||
|
||||
factory DownloadHistoryItem.fromJson(Map<String, dynamic> json) => DownloadHistoryItem(
|
||||
@@ -91,6 +99,8 @@ class DownloadHistoryItem {
|
||||
duration: json['duration'] as int?,
|
||||
releaseDate: json['releaseDate'] as String?,
|
||||
quality: json['quality'] as String?,
|
||||
bitDepth: json['bitDepth'] as int?,
|
||||
sampleRate: json['sampleRate'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -763,7 +773,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
if (track.coverUrl != null && track.coverUrl!.isNotEmpty) {
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final uniqueId = DateTime.now().millisecondsSinceEpoch;
|
||||
final uniqueId = '${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}';
|
||||
coverPath = '${tempDir.path}/cover_$uniqueId.jpg';
|
||||
|
||||
// Download cover using HTTP
|
||||
@@ -821,6 +831,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
if (track.isrc != null) {
|
||||
metadata['ISRC'] = track.isrc!;
|
||||
}
|
||||
|
||||
_log.d('Metadata map content: $metadata');
|
||||
|
||||
// Fetch Lyrics (Critical for M4A->FLAC conversion parity)
|
||||
// Since we are in the Flutter context, we can call the bridge to get lyrics
|
||||
@@ -1094,40 +1106,55 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
// If track number is missing/0 (common from Search results), fetch full metadata
|
||||
// This ensures the downloaded file has correct tags (Track, Disc, Year)
|
||||
Track trackToDownload = item.track;
|
||||
if (trackToDownload.trackNumber == null || trackToDownload.trackNumber == 0) {
|
||||
// Enrich metadata if ISRC or track number is missing (common from Search results)
|
||||
// ISRC is critical for accurate track matching on streaming services
|
||||
final needsEnrichment = trackToDownload.id.startsWith('deezer:') &&
|
||||
(trackToDownload.isrc == null || trackToDownload.isrc!.isEmpty ||
|
||||
trackToDownload.trackNumber == null || trackToDownload.trackNumber == 0);
|
||||
|
||||
if (needsEnrichment) {
|
||||
try {
|
||||
if (trackToDownload.id.startsWith('deezer:')) {
|
||||
_log.d('Enriching incomplete metadata for Deezer track: ${trackToDownload.name}');
|
||||
final rawId = trackToDownload.id.split(':')[1];
|
||||
final fullData = await PlatformBridge.getDeezerMetadata('track', rawId);
|
||||
|
||||
if (fullData.containsKey('track')) {
|
||||
final fullTrack = Track.fromJson(fullData['track'] as Map<String, dynamic>);
|
||||
// Merge with existing (keep override quality/service if any, but update metadata)
|
||||
_log.d('Enriching incomplete metadata for Deezer track: ${trackToDownload.name}');
|
||||
_log.d('Current ISRC: ${trackToDownload.isrc}, TrackNumber: ${trackToDownload.trackNumber}');
|
||||
final rawId = trackToDownload.id.split(':')[1];
|
||||
_log.d('Fetching full metadata for Deezer ID: $rawId');
|
||||
final fullData = await PlatformBridge.getDeezerMetadata('track', rawId);
|
||||
_log.d('Got response keys: ${fullData.keys.toList()}');
|
||||
|
||||
if (fullData.containsKey('track')) {
|
||||
// Parse Go backend response (snake_case) to Track
|
||||
final trackData = fullData['track'];
|
||||
_log.d('Track data type: ${trackData.runtimeType}');
|
||||
if (trackData is Map<String, dynamic>) {
|
||||
final data = trackData;
|
||||
_log.d('Track data keys: ${data.keys.toList()}');
|
||||
_log.d('ISRC from API: ${data['isrc']}');
|
||||
trackToDownload = Track(
|
||||
id: fullTrack.id.isNotEmpty ? fullTrack.id : trackToDownload.id,
|
||||
name: fullTrack.name,
|
||||
artistName: fullTrack.artistName,
|
||||
albumName: fullTrack.albumName,
|
||||
albumArtist: fullTrack.albumArtist,
|
||||
coverUrl: fullTrack.coverUrl,
|
||||
duration: fullTrack.duration,
|
||||
isrc: fullTrack.isrc ?? trackToDownload.isrc,
|
||||
trackNumber: fullTrack.trackNumber,
|
||||
discNumber: fullTrack.discNumber,
|
||||
releaseDate: fullTrack.releaseDate,
|
||||
deezerId: fullTrack.deezerId,
|
||||
id: (data['spotify_id'] as String?) ?? trackToDownload.id,
|
||||
name: (data['name'] as String?) ?? trackToDownload.name,
|
||||
artistName: (data['artists'] as String?) ?? trackToDownload.artistName,
|
||||
albumName: (data['album_name'] as String?) ?? trackToDownload.albumName,
|
||||
albumArtist: data['album_artist'] as String?,
|
||||
coverUrl: data['images'] as String?,
|
||||
// duration_ms from Go is in milliseconds, Track.duration is in seconds
|
||||
duration: ((data['duration_ms'] as int?) ?? (trackToDownload.duration * 1000)) ~/ 1000,
|
||||
isrc: (data['isrc'] as String?) ?? trackToDownload.isrc,
|
||||
trackNumber: data['track_number'] as int?,
|
||||
discNumber: data['disc_number'] as int?,
|
||||
releaseDate: data['release_date'] as String?,
|
||||
deezerId: rawId,
|
||||
availability: trackToDownload.availability,
|
||||
);
|
||||
_log.d('Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, Year ${trackToDownload.releaseDate}');
|
||||
|
||||
// Update item in state with enriched track
|
||||
// This is important so the UI (and history) reflects the enriched data
|
||||
// We don't perform a full `updateItemStatus` here to avoid UI flicker, just local var
|
||||
}
|
||||
_log.d('Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}');
|
||||
} else {
|
||||
_log.w('Unexpected track data type: ${trackData.runtimeType}');
|
||||
}
|
||||
} else {
|
||||
_log.w('Response does not contain track key');
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, stack) {
|
||||
_log.w('Failed to enrich metadata: $e');
|
||||
_log.w('Stack trace: $stack');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1257,7 +1284,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final backendYear = result['release_date'] as String?;
|
||||
final backendAlbum = result['album'] as String?;
|
||||
|
||||
// Create updated track object
|
||||
_log.d('Backend metadata - Track: $backendTrackNum, Disc: $backendDiscNum, Year: $backendYear');
|
||||
|
||||
// Create updated track object with safety check for 0/null
|
||||
final newTrackNumber = (backendTrackNum != null && backendTrackNum > 0) ? backendTrackNum : trackToDownload.trackNumber;
|
||||
final newDiscNumber = (backendDiscNum != null && backendDiscNum > 0) ? backendDiscNum : trackToDownload.discNumber;
|
||||
|
||||
_log.d('Final metadata for embedding - Track: $newTrackNumber, Disc: $newDiscNumber');
|
||||
|
||||
finalTrack = Track(
|
||||
id: trackToDownload.id,
|
||||
name: trackToDownload.name,
|
||||
@@ -1267,8 +1301,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
coverUrl: trackToDownload.coverUrl,
|
||||
duration: trackToDownload.duration,
|
||||
isrc: trackToDownload.isrc,
|
||||
trackNumber: (backendTrackNum != null && backendTrackNum > 0) ? backendTrackNum : trackToDownload.trackNumber,
|
||||
discNumber: (backendDiscNum != null && backendDiscNum > 0) ? backendDiscNum : trackToDownload.discNumber,
|
||||
trackNumber: newTrackNumber,
|
||||
discNumber: newDiscNumber,
|
||||
releaseDate: backendYear ?? trackToDownload.releaseDate,
|
||||
deezerId: trackToDownload.deezerId,
|
||||
availability: trackToDownload.availability,
|
||||
@@ -1337,6 +1371,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final backendYear = result['release_date'] as String?;
|
||||
final backendTrackNum = result['track_number'] as int?;
|
||||
final backendDiscNum = result['disc_number'] as int?;
|
||||
final backendBitDepth = result['actual_bit_depth'] as int?;
|
||||
final backendSampleRate = result['actual_sample_rate'] as int?;
|
||||
final backendISRC = result['isrc'] as String?;
|
||||
|
||||
ref.read(downloadHistoryProvider.notifier).addToHistory(
|
||||
DownloadHistoryItem(
|
||||
@@ -1350,13 +1387,15 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
service: result['service'] as String? ?? item.service,
|
||||
downloadedAt: DateTime.now(),
|
||||
// Additional metadata
|
||||
isrc: item.track.isrc,
|
||||
isrc: (backendISRC != null && backendISRC.isNotEmpty) ? backendISRC : item.track.isrc,
|
||||
spotifyId: item.track.id,
|
||||
trackNumber: (backendTrackNum != null && backendTrackNum > 0) ? backendTrackNum : item.track.trackNumber,
|
||||
discNumber: (backendDiscNum != null && backendDiscNum > 0) ? backendDiscNum : item.track.discNumber,
|
||||
duration: item.track.duration,
|
||||
releaseDate: (backendYear != null && backendYear.isNotEmpty) ? backendYear : item.track.releaseDate,
|
||||
quality: actualQuality,
|
||||
bitDepth: backendBitDepth,
|
||||
sampleRate: backendSampleRate,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -37,11 +37,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
final file = File(widget.item.filePath);
|
||||
final exists = await file.exists();
|
||||
int? size;
|
||||
|
||||
if (exists) {
|
||||
try {
|
||||
size = await file.length();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_fileExists = exists;
|
||||
@@ -55,7 +57,18 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// Use data directly from history item (cached from download)
|
||||
DownloadHistoryItem get item => widget.item;
|
||||
String get trackName => item.trackName;
|
||||
String get artistName => item.artistName;
|
||||
String get albumName => item.albumName;
|
||||
String? get albumArtist => item.albumArtist;
|
||||
int? get trackNumber => item.trackNumber;
|
||||
int? get discNumber => item.discNumber;
|
||||
String? get releaseDate => item.releaseDate;
|
||||
String? get isrc => item.isrc;
|
||||
int? get bitDepth => item.bitDepth;
|
||||
int? get sampleRate => item.sampleRate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -233,9 +246,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Track name
|
||||
// Track name (from file metadata)
|
||||
Text(
|
||||
item.trackName,
|
||||
trackName,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
@@ -243,16 +256,16 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Artist name
|
||||
// Artist name (from file metadata)
|
||||
Text(
|
||||
item.artistName,
|
||||
artistName,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Album name
|
||||
// Album name (from file metadata)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
@@ -263,7 +276,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.albumName,
|
||||
albumName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -401,28 +414,33 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
}
|
||||
|
||||
Widget _buildMetadataGrid(BuildContext context, ColorScheme colorScheme) {
|
||||
// Build audio quality string from file metadata
|
||||
String? audioQualityStr;
|
||||
if (bitDepth != null && sampleRate != null) {
|
||||
final sampleRateKHz = (sampleRate! / 1000).toStringAsFixed(1);
|
||||
audioQualityStr = '$bitDepth-bit/${sampleRateKHz}kHz';
|
||||
}
|
||||
|
||||
final items = <_MetadataItem>[
|
||||
_MetadataItem('Track name', item.trackName),
|
||||
_MetadataItem('Artist', item.artistName),
|
||||
if (item.albumArtist != null && item.albumArtist != item.artistName)
|
||||
_MetadataItem('Album artist', item.albumArtist!),
|
||||
_MetadataItem('Album', item.albumName),
|
||||
if (item.trackNumber != null)
|
||||
_MetadataItem('Track number', item.trackNumber.toString()),
|
||||
if (item.discNumber != null && item.discNumber! > 1)
|
||||
_MetadataItem('Disc number', item.discNumber.toString()),
|
||||
_MetadataItem('Track name', trackName),
|
||||
_MetadataItem('Artist', artistName),
|
||||
if (albumArtist != null && albumArtist != artistName)
|
||||
_MetadataItem('Album artist', albumArtist!),
|
||||
_MetadataItem('Album', albumName),
|
||||
if (trackNumber != null && trackNumber! > 0)
|
||||
_MetadataItem('Track number', trackNumber.toString()),
|
||||
if (discNumber != null && discNumber! > 1)
|
||||
_MetadataItem('Disc number', discNumber.toString()),
|
||||
if (item.duration != null)
|
||||
_MetadataItem('Duration', _formatDuration(item.duration!)),
|
||||
if (item.quality != null && item.quality!.contains('bit'))
|
||||
_MetadataItem('Audio quality', item.quality!),
|
||||
if (item.releaseDate != null && item.releaseDate!.isNotEmpty)
|
||||
_MetadataItem('Release date', item.releaseDate!),
|
||||
if (item.isrc != null && item.isrc!.isNotEmpty)
|
||||
_MetadataItem('ISRC', item.isrc!),
|
||||
if (audioQualityStr != null)
|
||||
_MetadataItem('Audio quality', audioQualityStr),
|
||||
if (releaseDate != null && releaseDate!.isNotEmpty)
|
||||
_MetadataItem('Release date', releaseDate!),
|
||||
if (isrc != null && isrc!.isNotEmpty)
|
||||
_MetadataItem('ISRC', isrc!),
|
||||
if (item.spotifyId != null && item.spotifyId!.isNotEmpty)
|
||||
_MetadataItem('Spotify ID', item.spotifyId!),
|
||||
if (item.quality != null && item.quality!.isNotEmpty)
|
||||
_MetadataItem('Quality', _formatQuality(item.quality!)),
|
||||
_MetadataItem('Service', item.service.toUpperCase()),
|
||||
_MetadataItem('Downloaded', _formatFullDate(item.downloadedAt)),
|
||||
];
|
||||
@@ -476,32 +494,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
return '$minutes:${secs.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _formatQuality(String quality) {
|
||||
switch (quality) {
|
||||
case 'LOSSLESS':
|
||||
return 'Lossless (16-bit)';
|
||||
case 'HI_RES':
|
||||
return 'Hi-Res (24-bit)';
|
||||
case 'HI_RES_LOSSLESS':
|
||||
return 'Hi-Res Lossless (24-bit)';
|
||||
default:
|
||||
return quality;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatQualityShort(String quality) {
|
||||
switch (quality) {
|
||||
case 'LOSSLESS':
|
||||
return '16-bit';
|
||||
case 'HI_RES':
|
||||
return '24-bit';
|
||||
case 'HI_RES_LOSSLESS':
|
||||
return 'Hi-Res';
|
||||
default:
|
||||
return quality;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFileInfoCard(BuildContext context, ColorScheme colorScheme, bool fileExists, int? fileSize) {
|
||||
final fileName = item.filePath.split(Platform.pathSeparator).last;
|
||||
final fileExtension = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : 'Unknown';
|
||||
@@ -570,7 +562,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item.quality != null)
|
||||
if (bitDepth != null && sampleRate != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
@@ -578,7 +570,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_formatQualityShort(item.quality!),
|
||||
'$bitDepth-bit/${(sampleRate! / 1000).toStringAsFixed(1)}kHz',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -891,7 +883,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.delete, color: colorScheme.error),
|
||||
title: Text('Remove from history', style: TextStyle(color: colorScheme.error)),
|
||||
title: Text('Remove from device', style: TextStyle(color: colorScheme.error)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_confirmDelete(context, ref, colorScheme);
|
||||
@@ -908,10 +900,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Remove from history?'),
|
||||
title: const Text('Remove from device?'),
|
||||
content: const Text(
|
||||
'This will remove the track from your download history. '
|
||||
'The downloaded file will not be deleted.',
|
||||
'This will permanently delete the downloaded file and remove it from your history.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -919,12 +910,26 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
// Delete the file first
|
||||
try {
|
||||
final file = File(item.filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to delete file: $e');
|
||||
}
|
||||
|
||||
// Remove from history
|
||||
ref.read(downloadHistoryProvider.notifier).removeFromHistory(item.id);
|
||||
Navigator.pop(context); // Close dialog
|
||||
Navigator.pop(context); // Go back to history
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // Close dialog
|
||||
Navigator.pop(context); // Go back to history
|
||||
}
|
||||
},
|
||||
child: Text('Remove', style: TextStyle(color: colorScheme.error)),
|
||||
child: Text('Delete', style: TextStyle(color: colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -111,7 +111,7 @@ class PlatformBridge {
|
||||
int discNumber = 1,
|
||||
int totalTracks = 1,
|
||||
String? releaseDate,
|
||||
String preferredService = 'qobuz',
|
||||
String preferredService = 'tidal',
|
||||
String? itemId,
|
||||
int durationMs = 0,
|
||||
}) async {
|
||||
@@ -248,6 +248,16 @@ class PlatformBridge {
|
||||
await _channel.invokeMethod('cleanupConnections');
|
||||
}
|
||||
|
||||
/// Read metadata directly from a FLAC file
|
||||
/// Returns all embedded metadata (title, artist, album, track number, etc.)
|
||||
/// This reads from the actual file, not from cached/database data
|
||||
static Future<Map<String, dynamic>> readFileMetadata(String filePath) async {
|
||||
final result = await _channel.invokeMethod('readFileMetadata', {
|
||||
'file_path': filePath,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Start foreground download service to keep downloads running in background
|
||||
static Future<void> startDownloadService({
|
||||
String trackName = '',
|
||||
@@ -335,6 +345,9 @@ class PlatformBridge {
|
||||
'resource_type': resourceType,
|
||||
'resource_id': resourceId,
|
||||
});
|
||||
if (result == null) {
|
||||
throw Exception('getDeezerMetadata returned null for $resourceType:$resourceId');
|
||||
}
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user