import 'dart:async'; import 'dart:io'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('ShareIntent'); class ShareIntentService { static final ShareIntentService _instance = ShareIntentService._internal(); 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/|browse/)[a-zA-Z0-9_-]+([?&][^\s]*)?', ); // Standard YouTube patterns (youtu.be short links and www.youtube.com/watch) static final RegExp _youtubeUrlPattern = RegExp( r'https?://(youtu\.be/[a-zA-Z0-9_-]+|www\.youtube\.com/watch\?v=[a-zA-Z0-9_-]+)([?&][^\s]*)?', ); final _sharedUrlController = StreamController.broadcast(); StreamSubscription>? _mediaSubscription; bool _initialized = false; String? _pendingUrl; Stream get sharedUrlStream => _sharedUrlController.stream; String? consumePendingUrl() { final url = _pendingUrl; _pendingUrl = null; return url; } Future initialize() async { if (_initialized) return; _initialized = true; if (!Platform.isAndroid && !Platform.isIOS) { _log.i('Share intent is not supported on this platform'); return; } _mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen( _handleSharedMedia, onError: (Object err) => _log.e('Error: $err'), ); final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia(); if (initialMedia.isNotEmpty) { _handleSharedMedia(initialMedia, isInitial: true); ReceiveSharingIntent.instance.reset(); } } void _handleSharedMedia( List files, { bool isInitial = false, }) { for (final file in files) { final textsToCheck = [file.path, if (file.message != null) file.message!]; for (final textToCheck in textsToCheck) { final url = _extractMusicUrl(textToCheck); if (url != null) { _log.i('Received music URL: $url (initial: $isInitial)'); if (isInitial) { _pendingUrl = url; } _sharedUrlController.add(url); return; } } } } String? _extractMusicUrl(String text) { if (text.isEmpty) return null; final uriMatch = _spotifyUriPattern.firstMatch(text); if (uriMatch != null) { return uriMatch.group(0); } final patterns = [ _spotifyUrlPattern, _deezerUrlPattern, _deezerShortLinkPattern, _tidalUrlPattern, _ytMusicUrlPattern, _youtubeUrlPattern, ]; for (final pattern in patterns) { final match = pattern.firstMatch(text); if (match != null) { final fullUrl = match.group(0)!; // Keep query params for YouTube URLs (needed for ?v=, ?list=, etc.) if (pattern == _ytMusicUrlPattern || pattern == _youtubeUrlPattern) { return fullUrl; } final queryIndex = fullUrl.indexOf('?'); return queryIndex > 0 ? fullUrl.substring(0, queryIndex) : fullUrl; } } return null; } void dispose() { _mediaSubscription?.cancel(); _sharedUrlController.close(); } }