feat: add YouTube provider for lossy downloads via Cobalt API

- New YouTube download provider with Opus 256kbps and MP3 320kbps options
- SongLink/Odesli integration for Spotify/Deezer ID to YouTube URL conversion
- YouTube video ID detection for YT Music extension compatibility
- Parallel cover art and lyrics fetching during download
- Queue progress shows bytes (X.X MB) for streaming downloads
- Full metadata embedding: cover, lyrics, title, artist, album, track#, disc#, year, ISRC
- Removed Tidal HIGH (lossy AAC) option - use YouTube for lossy instead
- Bumped version to 3.6.0
This commit is contained in:
zarzet
2026-02-09 18:15:43 +07:00
parent 23f5aa11b0
commit 7ec5d28caf
30 changed files with 1225 additions and 166 deletions
+68 -5
View File
@@ -1113,8 +1113,71 @@ static Future<Map<String, dynamic>> downloadWithExtensions({
return result as String;
}
static Future<void> clearStoreCache() async {
_log.d('clearStoreCache');
await _channel.invokeMethod('clearStoreCache');
}
}
static Future<void> clearStoreCache() async {
_log.d('clearStoreCache');
await _channel.invokeMethod('clearStoreCache');
}
// ==================== YOUTUBE / COBALT ====================
/// Download a track from YouTube using the Cobalt API.
/// YouTube is a lossy-only provider (Opus 256kbps or MP3 320kbps).
/// It does NOT participate in the lossless fallback chain.
static Future<Map<String, dynamic>> downloadFromYouTube({
required String trackName,
required String artistName,
required String albumName,
String? albumArtist,
String? coverUrl,
required String outputDir,
required String filenameFormat,
String quality = 'opus_256',
int trackNumber = 1,
int discNumber = 1,
String? releaseDate,
String? itemId,
int durationMs = 0,
String? isrc,
String? spotifyId,
String? deezerId,
String storageMode = 'app',
String safTreeUri = '',
String safRelativeDir = '',
String safFileName = '',
String safOutputExt = '',
}) async {
_log.i('downloadFromYouTube: "$trackName" by $artistName (quality: $quality)');
final request = jsonEncode({
'track_name': trackName,
'artist_name': artistName,
'album_name': albumName,
'album_artist': albumArtist ?? artistName,
'cover_url': coverUrl,
'output_dir': outputDir,
'filename_format': filenameFormat,
'quality': quality,
'track_number': trackNumber,
'disc_number': discNumber,
'release_date': releaseDate ?? '',
'item_id': itemId ?? '',
'duration_ms': durationMs,
'isrc': isrc ?? '',
'spotify_id': spotifyId ?? '',
'deezer_id': deezerId ?? '',
'storage_mode': storageMode,
'saf_tree_uri': safTreeUri,
'saf_relative_dir': safRelativeDir,
'saf_file_name': safFileName,
'saf_output_ext': safOutputExt,
});
final result = await _channel.invokeMethod('downloadFromYouTube', request);
final response = jsonDecode(result as String) as Map<String, dynamic>;
if (response['success'] == true) {
_log.i('YouTube download success: ${response['file_path']}');
} else {
_log.w('YouTube download failed: ${response['error']}');
}
return response;
}
}