mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-09 22:18:45 +02:00
feat: add support for Deezer, Tidal, and YT Music links
This commit is contained in:
@@ -193,6 +193,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
|
||||
|
||||
try {
|
||||
// Step 1: Check for extension URL handlers first (handles YT Music, etc.)
|
||||
final extensionHandler = await PlatformBridge.findURLHandler(url);
|
||||
if (extensionHandler != null) {
|
||||
_log.i('Found extension URL handler: $extensionHandler for URL: $url');
|
||||
@@ -251,8 +252,131 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Try Deezer URL parsing
|
||||
if (url.contains('deezer.com') || url.contains('deezer.page.link')) {
|
||||
_log.i('Detected Deezer URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseDeezerUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
final id = parsed['id'] as String;
|
||||
|
||||
final metadata = await PlatformBridge.getDeezerMetadata(type, id);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
if (type == 'track') {
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
} else if (type == 'album') {
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList.map((t) => _parseTrack(t as Map<String, dynamic>)).toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
albumId: id,
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: albumInfo['images'] as String?,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'playlist') {
|
||||
final playlistInfo = metadata['playlist_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList.map((t) => _parseTrack(t as Map<String, dynamic>)).toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
playlistName: playlistInfo['name'] as String?,
|
||||
coverUrl: playlistInfo['images'] as String?,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'artist') {
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList.map((a) => _parseArtistAlbum(a as Map<String, dynamic>)).toList();
|
||||
state = TrackState(
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
coverUrl: artistInfo['images'] as String?,
|
||||
artistAlbums: albums,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Try Tidal URL parsing
|
||||
if (url.contains('tidal.com')) {
|
||||
_log.i('Detected Tidal URL, parsing...');
|
||||
final parsed = await PlatformBridge.parseTidalUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
final id = parsed['id'] as String;
|
||||
|
||||
_log.i('Tidal URL parsed: type=$type, id=$id');
|
||||
|
||||
// For track URLs, convert to Spotify/Deezer and fetch metadata from there
|
||||
if (type == 'track') {
|
||||
try {
|
||||
_log.i('Converting Tidal track to Spotify/Deezer via SongLink...');
|
||||
final conversion = await PlatformBridge.convertTidalToSpotifyDeezer(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final spotifyUrl = conversion['spotify_url'] as String?;
|
||||
final deezerUrl = conversion['deezer_url'] as String?;
|
||||
|
||||
if (spotifyUrl != null && spotifyUrl.isNotEmpty) {
|
||||
_log.i('Found Spotify URL: $spotifyUrl, fetching metadata...');
|
||||
final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(spotifyUrl);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
return;
|
||||
} else if (deezerUrl != null && deezerUrl.isNotEmpty) {
|
||||
_log.i('Found Deezer URL: $deezerUrl, fetching metadata...');
|
||||
final deezerParsed = await PlatformBridge.parseDeezerUrl(deezerUrl);
|
||||
final metadata = await PlatformBridge.getDeezerMetadata('track', deezerParsed['id'] as String);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Failed to convert Tidal URL via SongLink: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// For album/artist/playlist, not yet supported
|
||||
state = TrackState(
|
||||
isLoading: false,
|
||||
error: 'Tidal $type links are not fully supported yet. Only track links work via SongLink conversion.',
|
||||
hasSearchText: state.hasSearchText,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: Fall back to Spotify parsing
|
||||
final parsed = await PlatformBridge.parseSpotifyUrl(url);
|
||||
if (!_isRequestValid(requestId)) return; // Request cancelled
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
|
||||
@@ -264,7 +388,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (!_isRequestValid(requestId)) return; // Request cancelled
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
if (type == 'track') {
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
|
||||
@@ -368,6 +368,16 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> parseTidalUrl(String url) async {
|
||||
final result = await _channel.invokeMethod('parseTidalUrl', {'url': url});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> convertTidalToSpotifyDeezer(String tidalUrl) async {
|
||||
final result = await _channel.invokeMethod('convertTidalToSpotifyDeezer', {'url': tidalUrl});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchDeezerByISRC(String isrc) async {
|
||||
final result = await _channel.invokeMethod('searchDeezerByISRC', {'isrc': isrc});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
|
||||
@@ -9,16 +9,35 @@ class ShareIntentService {
|
||||
factory ShareIntentService() => _instance;
|
||||
ShareIntentService._internal();
|
||||
|
||||
// Spotify patterns
|
||||
static final RegExp _spotifyUriPattern =
|
||||
RegExp(r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+');
|
||||
static final RegExp _spotifyUrlPattern = RegExp(
|
||||
r'https?://open\.spotify\.com/(track|album|playlist|artist)/[a-zA-Z0-9]+(\?[^\s]*)?',
|
||||
);
|
||||
|
||||
// Deezer patterns
|
||||
static final RegExp _deezerUrlPattern = RegExp(
|
||||
r'https?://(www\.)?deezer\.com/(track|album|playlist|artist)/\d+(\?[^\s]*)?',
|
||||
);
|
||||
static final RegExp _deezerShortLinkPattern = RegExp(
|
||||
r'https?://deezer\.page\.link/[a-zA-Z0-9]+',
|
||||
);
|
||||
|
||||
// Tidal patterns
|
||||
static final RegExp _tidalUrlPattern = RegExp(
|
||||
r'https?://(listen\.)?tidal\.com/(track|album|playlist|artist)/[a-zA-Z0-9-]+(\?[^\s]*)?',
|
||||
);
|
||||
|
||||
// YouTube Music patterns
|
||||
static final RegExp _ytMusicUrlPattern = RegExp(
|
||||
r'https?://music\.youtube\.com/(watch\?v=|playlist\?list=|channel/)[a-zA-Z0-9_-]+(\&[^\s]*)?',
|
||||
);
|
||||
|
||||
final _sharedUrlController = StreamController<String>.broadcast();
|
||||
StreamSubscription<List<SharedMediaFile>>? _mediaSubscription;
|
||||
bool _initialized = false;
|
||||
String? _pendingUrl; // Store URL received before listener is ready
|
||||
String? _pendingUrl;
|
||||
|
||||
Stream<String> get sharedUrlStream => _sharedUrlController.stream;
|
||||
|
||||
@@ -48,31 +67,47 @@ class ShareIntentService {
|
||||
for (final file in files) {
|
||||
final textToCheck = file.path;
|
||||
|
||||
final url = _extractSpotifyUrl(textToCheck);
|
||||
final url = _extractMusicUrl(textToCheck);
|
||||
if (url != null) {
|
||||
_log.i('Received Spotify URL: $url (initial: $isInitial)');
|
||||
_log.i('Received music URL: $url (initial: $isInitial)');
|
||||
if (isInitial) {
|
||||
_pendingUrl = url;
|
||||
}
|
||||
_sharedUrlController.add(url);
|
||||
return; // Only process first valid URL
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _extractSpotifyUrl(String text) {
|
||||
String? _extractMusicUrl(String text) {
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
// Try Spotify URI first
|
||||
final uriMatch = _spotifyUriPattern.firstMatch(text);
|
||||
if (uriMatch != null) {
|
||||
return uriMatch.group(0);
|
||||
}
|
||||
|
||||
final urlMatch = _spotifyUrlPattern.firstMatch(text);
|
||||
if (urlMatch != null) {
|
||||
final fullUrl = urlMatch.group(0)!;
|
||||
final queryIndex = fullUrl.indexOf('?');
|
||||
return queryIndex > 0 ? fullUrl.substring(0, queryIndex) : fullUrl;
|
||||
// Try all URL patterns
|
||||
final patterns = [
|
||||
_spotifyUrlPattern,
|
||||
_deezerUrlPattern,
|
||||
_deezerShortLinkPattern,
|
||||
_tidalUrlPattern,
|
||||
_ytMusicUrlPattern,
|
||||
];
|
||||
|
||||
for (final pattern in patterns) {
|
||||
final match = pattern.firstMatch(text);
|
||||
if (match != null) {
|
||||
final fullUrl = match.group(0)!;
|
||||
// Remove query params for cleaner URL (except for YT Music which needs them)
|
||||
if (pattern == _ytMusicUrlPattern) {
|
||||
return fullUrl;
|
||||
}
|
||||
final queryIndex = fullUrl.indexOf('?');
|
||||
return queryIndex > 0 ? fullUrl.substring(0, queryIndex) : fullUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user