From 698fb6ba2793eb1332dfa00f3f94e7c78646681b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 5 Feb 2025 00:36:23 +0600 Subject: [PATCH] fix: youtube tracks keeps skipping despite being matched correctly --- lib/pages/settings/sections/playback.dart | 2 +- .../audio_player/audio_player_streams.dart | 12 ++-- lib/provider/server/routes/playback.dart | 58 ++++++++++++++----- lib/provider/server/sourced_track.dart | 13 +++++ lib/services/logger/logger.dart | 2 +- .../sourced_track/sources/youtube.dart | 24 +++++++- 6 files changed, 90 insertions(+), 21 deletions(-) diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 537156d0..363e228c 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -197,7 +197,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: preferences.audioSource != AudioSource.youtube + crossFadeState: preferences.audioSource == AudioSource.youtube ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: const SizedBox.shrink(), diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 880f643f..54c6d7cd 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/local_track.dart'; @@ -103,16 +104,19 @@ class AudioPlayerStreamListeners { StreamSubscription subscribeToPosition() { String lastTrack = ""; // used to prevent multiple calls to the same track return audioPlayer.positionStream.listen((event) async { + final percentProgress = + (event.inSeconds / max(audioPlayer.duration.inSeconds, 1)) * 100; try { - if (event < const Duration(seconds: 3) || + if (percentProgress < 80 || audioPlayerState.playlist.index == -1 || audioPlayerState.playlist.index == audioPlayerState.tracks.length - 1) { return; } - final nextTrack = SpotubeMedia.fromMedia(audioPlayerState - .playlist.medias - .elementAt(audioPlayerState.playlist.index + 1)); + final nextTrack = SpotubeMedia.fromMedia( + audioPlayerState.playlist.medias + .elementAt(audioPlayerState.playlist.index + 1), + ); if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { return; diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 289da0e3..3a480248 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:dio/dio.dart' hide Response; import 'package:dio/dio.dart' as dio_lib; @@ -10,6 +11,7 @@ import 'package:shelf/shelf.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/track.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/parser/range_headers.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; @@ -22,6 +24,20 @@ import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +const _deviceClients = { + YoutubeApiClient.android, + YoutubeApiClient.ios, + YoutubeApiClient.mweb, + YoutubeApiClient.safari, +}; + +String? get _randomUserAgent => _deviceClients + .elementAt( + Random().nextInt(_deviceClients.length), + ) + .payload["context"]["client"]["userAgent"]; class ServerPlaybackRoutes { final Ref ref; @@ -47,9 +63,8 @@ class ServerPlaybackRoutes { var options = Options( headers: { ...headers, - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", - "Cache-Control": "max-age=0", + "user-agent": _randomUserAgent, + "Cache-Control": "max-age=3600", "Connection": "keep-alive", "host": Uri.parse(track.url).host, }, @@ -100,18 +115,35 @@ class ServerPlaybackRoutes { ); } - final res = - await dio.get(track.url, options: options).catchError( - (e, stack) async { - final sourcedTrack = await ref - .read(sourcedTrackProvider(SpotubeMedia(track)).notifier) - .switchToAlternativeSources(); + final res = await dio + .get( + track.url, + options: options.copyWith(headers: { + ...?options.headers, + "user-agent": _randomUserAgent, + }), + ) + .catchError((e, stack) async { + AppLogger.reportError(e, stack); + final sourcedTrack = userPreferences.audioSource == AudioSource.youtube && + e is DioException + ? await ref + .read(sourcedTrackProvider(SpotubeMedia(track)).notifier) + .refreshStreamingUrl() + : await ref + .read(sourcedTrackProvider(SpotubeMedia(track)).notifier) + .switchToAlternativeSources(); - ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); + ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); - return await dio.get(sourcedTrack!.url, options: options); - }, - ); + return await dio.get( + sourcedTrack!.url, + options: options.copyWith(headers: { + ...?options.headers, + "user-agent": _randomUserAgent, + }), + ); + }); final bytes = res.data; diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart index 58531523..2081ac0a 100644 --- a/lib/provider/server/sourced_track.dart +++ b/lib/provider/server/sourced_track.dart @@ -29,6 +29,19 @@ class SourcedTrackNotifier return sourcedTrack; } + Future refreshStreamingUrl() async { + if (arg == null) { + return null; + } + + return await update((prev) async { + return await SourcedTrack.fetchFromTrack( + track: state.value!, + ref: ref, + ); + }); + } + Future switchToAlternativeSources() async { if (arg == null) { return null; diff --git a/lib/services/logger/logger.dart b/lib/services/logger/logger.dart index d1595930..1f15bf92 100644 --- a/lib/services/logger/logger.dart +++ b/lib/services/logger/logger.dart @@ -38,7 +38,7 @@ class AppLogger { if (!kDebugMode) return; logging.hierarchicalLoggingEnabled = true; logging.Logger('YoutubeExplode.StreamsClient') - ..level = logging.Level.ALL + ..level = logging.Level.SEVERE ..onRecord.listen( (record) { log.log( diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 0b5e1b2a..f54b1772 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -50,7 +50,6 @@ class YoutubeSourcedTrack extends SourcedTrack { ytClients: [ YoutubeApiClient.android, YoutubeApiClient.mweb, - YoutubeApiClient.safari, ], ); } @@ -59,6 +58,23 @@ class YoutubeSourcedTrack extends SourcedTrack { required Track track, required Ref ref, }) async { + // Indicates the track is requesting a stream refresh + if (track is YoutubeSourcedTrack) { + final manifest = await _getStreamManifest(track.sourceInfo.id); + + final sourcedTrack = YoutubeSourcedTrack( + ref: ref, + siblings: track.siblings, + source: toSourceMap(manifest), + sourceInfo: track.sourceInfo, + track: track, + ); + + AppLogger.log.i("Refreshing ${track.name}: ${sourcedTrack.url}"); + + return sourcedTrack; + } + final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) ..where((s) => s.trackId.equals(track.id!)) @@ -94,7 +110,7 @@ class YoutubeSourcedTrack extends SourcedTrack { } final item = await youtubeClient.videos.get(cachedSource.sourceId); final manifest = await _getStreamManifest(cachedSource.sourceId); - return YoutubeSourcedTrack( + final sourcedTrack = YoutubeSourcedTrack( ref: ref, siblings: [], source: toSourceMap(manifest), @@ -110,6 +126,10 @@ class YoutubeSourcedTrack extends SourcedTrack { ), track: track, ); + + AppLogger.log.i("${track.name}: ${sourcedTrack.url}"); + + return sourcedTrack; } static SourceMap toSourceMap(StreamManifest manifest) {