From ea329f40e88c9ce4210e381771c9cd256c5af422 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 2 Sep 2025 20:56:28 +0600 Subject: [PATCH] fix(yt): fallback to different search result if all streaming url is inaccessible --- .../dialogs/track_details_dialog.dart | 4 +-- lib/provider/download_manager_provider.dart | 20 ++++++++------- lib/provider/server/routes/playback.dart | 17 +++++++++---- lib/provider/server/track_sources.dart | 6 +++++ lib/services/sourced_track/sourced_track.dart | 8 +++--- .../sourced_track/sources/youtube.dart | 25 +++++++++++++------ .../youtube_explode_engine.dart | 20 ++++++++++++++- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart index 4e686c06..3d3fd7e9 100644 --- a/lib/components/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -60,8 +60,8 @@ class TrackDetailsDialog extends HookConsumerWidget { context.l10n.channel: Text(sourceInfo.artists), if (sourcedTrack.asData?.value.url != null) context.l10n.streamUrl: Hyperlink( - sourcedTrack.asData!.value.url, - sourcedTrack.asData!.value.url, + sourcedTrack.asData!.value.url ?? "", + sourcedTrack.asData!.value.url ?? "", maxLines: 2, overflow: TextOverflow.ellipsis, ), diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 2a79f60d..d7f28b67 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -130,7 +130,7 @@ class DownloadManagerProvider extends ChangeNotifier { download.status.value == DownloadStatus.queued, ) .map((e) => e.request.url) - .contains(sourcedTrack.getUrlOfCodec(downloadCodec)); + .contains(sourcedTrack.getUrlOfCodec(downloadCodec)!); } /// For singular downloads @@ -152,7 +152,9 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack.codec == downloadCodec) { final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfCodec(downloadCodec), savePath); + sourcedTrack.getUrlOfCodec(downloadCodec)!, + savePath, + ); if (downloadTask != null) { $history.add(sourcedTrack); } @@ -169,7 +171,7 @@ class DownloadManagerProvider extends ChangeNotifier { return d; }); final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfCodec(downloadCodec), + sourcedTrack.getUrlOfCodec(downloadCodec)!, savePath, ); if (downloadTask != null) { @@ -202,18 +204,18 @@ class DownloadManagerProvider extends ChangeNotifier { Future removeFromQueue(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); + await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); $history.remove(sourcedTrack); } Future pause(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); + return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); } Future resume(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); + return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); } Future retry(SpotubeFullTrackObject track) { @@ -222,7 +224,7 @@ class DownloadManagerProvider extends ChangeNotifier { void cancel(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); + return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); } void cancelAll() { @@ -256,7 +258,7 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack == null) { return null; } - return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec))?.status; + return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.status; } ValueNotifier? getProgressNotifier(SpotubeFullTrackObject track) { @@ -266,7 +268,7 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack == null) { return null; } - return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec))?.progress; + return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.progress; } } diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index e2b90220..a158c86c 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -61,13 +61,20 @@ class ServerPlaybackRoutes { ); final trackPartialCacheFile = File("${trackCacheFile.path}.part"); + String? url = track.url; + + url ??= await ref + .read(trackSourcesProvider(track.query).notifier) + .swapWithNextSibling() + .then((track) => track.url!); + var options = Options( headers: { ...headers, "user-agent": _randomUserAgent, "Cache-Control": "max-age=3600", "Connection": "keep-alive", - "host": Uri.parse(track.url).host, + "host": Uri.parse(url!).host, }, responseType: ResponseType.bytes, validateStatus: (status) => status! < 400, @@ -75,7 +82,7 @@ class ServerPlaybackRoutes { final headersRes = await Future.value( dio.head( - track.url, + url, options: options, ), ).catchError((_) async => null); @@ -95,7 +102,7 @@ class ServerPlaybackRoutes { "accept-ranges": ["bytes"], "content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], }), - requestOptions: RequestOptions(path: track.url), + requestOptions: RequestOptions(path: url), ), bytes: bytes, ); @@ -118,7 +125,7 @@ class ServerPlaybackRoutes { final res = await dio .get( - track.url, + url, options: options.copyWith(headers: { ...?options.headers, "user-agent": _randomUserAgent, @@ -136,7 +143,7 @@ class ServerPlaybackRoutes { // } return await dio.get( - sourcedTrack.url, + sourcedTrack.url!, options: options.copyWith(headers: { ...?options.headers, "user-agent": _randomUserAgent, diff --git a/lib/provider/server/track_sources.dart b/lib/provider/server/track_sources.dart index 4a0f29ca..24502471 100644 --- a/lib/provider/server/track_sources.dart +++ b/lib/provider/server/track_sources.dart @@ -34,6 +34,12 @@ class TrackSourcesNotifier return await prev.swapWithSibling(sibling) ?? prev; }); } + + Future swapWithNextSibling() async { + return await update((prev) async { + return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack; + }); + } } final trackSourcesProvider = AsyncNotifierProviderFamily refreshStream(); - String get url { + String? get url { final preferences = ref.read(userPreferencesProvider); final codec = preferences.audioSource == AudioSource.jiosaavn @@ -157,7 +157,7 @@ abstract class SourcedTrack extends BasicSourcedTrack { /// /// If no sources match the codec, it will return the first or last source /// based on the user's audio quality preference. - String getUrlOfCodec(SourceCodecs codec) { + String? getUrlOfCodec(SourceCodecs codec) { final preferences = ref.read(userPreferencesProvider); final exactMatch = sources.firstWhereOrNull( @@ -191,8 +191,8 @@ abstract class SourcedTrack extends BasicSourcedTrack { }); return preferences.audioQuality != SourceQualities.low - ? fallbackSource.first.url - : fallbackSource.last.url; + ? fallbackSource.firstOrNull?.url + : fallbackSource.lastOrNull?.url; } SourceCodecs get codec { diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index e7899266..66b4c5d6 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -204,6 +204,8 @@ class YoutubeSourcedTrack extends SourcedTrack { AppLogger.log .d("${track.title} ISRC $isrc Total ${searchedVideos.length}"); + final stringBuffer = StringBuffer(); + final filteredMatches = searchedVideos .map(YoutubeVideoInfo.fromVideo) .map((YoutubeVideoInfo videoInfo) { @@ -224,6 +226,10 @@ class YoutubeSourcedTrack extends SourcedTrack { .abs() .inMilliseconds <= 3000) { + stringBuffer.writeln( + "ISRC MATCH: ${videoInfo.id} ${videoInfo.title} by ${videoInfo.channelName} ${videoInfo.duration}", + ); + return videoInfo; } return null; @@ -231,11 +237,7 @@ class YoutubeSourcedTrack extends SourcedTrack { .nonNulls .toList(); - for (final match in filteredMatches) { - AppLogger.log.d( - "ISRC MATCH: ${match.id} ${match.title} by ${match.channelName} ${match.duration}", - ); - } + AppLogger.log.d(stringBuffer.toString()); isrcResults.addAll(filteredMatches); } @@ -262,9 +264,16 @@ class YoutubeSourcedTrack extends SourcedTrack { final links = await SongLinkService.links(query.id); - for (final link in links) { - AppLogger.log.d("SongLink ${query.id} ${link.platform} ${link.url}"); - } + final stringBuffer = links.fold( + StringBuffer(), + (previousValue, element) { + previousValue.writeln( + "SongLink ${query.id} ${element.platform} ${element.url}"); + return previousValue; + }, + ); + + AppLogger.log.d(stringBuffer.toString()); final ytLink = links.firstWhereOrNull( (link) => link.platform == "youtube", diff --git a/lib/services/youtube_engine/youtube_explode_engine.dart b/lib/services/youtube_engine/youtube_explode_engine.dart index 26753e83..2160c629 100644 --- a/lib/services/youtube_engine/youtube_explode_engine.dart +++ b/lib/services/youtube_engine/youtube_explode_engine.dart @@ -1,6 +1,7 @@ import 'dart:isolate'; import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/youtube_engine/youtube_engine.dart'; @@ -60,6 +61,7 @@ class IsolatedYoutubeExplode { static void _isolateEntry(SendPort mainSendPort) { final receivePort = ReceivePort(); final youtubeExplode = YoutubeExplode(); + final stopWatch = kDebugMode ? Stopwatch() : null; /// Send the main port to the main isolate mainSendPort.send(receivePort.sendPort); @@ -69,6 +71,19 @@ class IsolatedYoutubeExplode { final String methodName = message[1]; final List arguments = message[2]; + if (stopWatch != null) { + if (stopWatch.isRunning) { + stopWatch.stop(); + final symbol = stopWatch.elapsedMilliseconds < 1000 ? "⚠️" : "⏱️"; + debugPrint( + "$symbol YoutubeExplode operation gap ${stopWatch.elapsedMilliseconds} ms", + ); + stopWatch.reset(); + } else { + stopWatch.start(); + } + } + // Run the requested method on YoutubeExplode var result = switch (methodName) { "search" => youtubeExplode.search @@ -156,6 +171,7 @@ class YouTubeExplodeEngine implements YouTubeEngine { ); final accessibleStreams = []; + final stringBuffer = StringBuffer(); for (final stream in streamManifest.audioOnly) { // Call dio head request to check if the stream is accessible @@ -169,7 +185,7 @@ class YouTubeExplodeEngine implements YouTubeEngine { ), ); - AppLogger.log.d( + stringBuffer.writeln( "Stream $videoId Status ${response.statusCode} Codec ${stream.audioCodec} " "Bitrate ${stream.bitrate} Container ${stream.container}", ); @@ -199,6 +215,8 @@ class YouTubeExplodeEngine implements YouTubeEngine { } } + AppLogger.log.d(stringBuffer.toString()); + return StreamManifest(accessibleStreams); }