fix: youtube tracks keeps skipping despite being matched correctly

This commit is contained in:
Kingkor Roy Tirtho 2025-02-05 00:36:23 +06:00
parent 043eaba81f
commit 698fb6ba27
6 changed files with 90 additions and 21 deletions

View File

@ -197,7 +197,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
), ),
AnimatedCrossFade( AnimatedCrossFade(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
crossFadeState: preferences.audioSource != AudioSource.youtube crossFadeState: preferences.audioSource == AudioSource.youtube
? CrossFadeState.showFirst ? CrossFadeState.showFirst
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: const SizedBox.shrink(), firstChild: const SizedBox.shrink(),

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
@ -103,16 +104,19 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPosition() { StreamSubscription subscribeToPosition() {
String lastTrack = ""; // used to prevent multiple calls to the same track String lastTrack = ""; // used to prevent multiple calls to the same track
return audioPlayer.positionStream.listen((event) async { return audioPlayer.positionStream.listen((event) async {
final percentProgress =
(event.inSeconds / max(audioPlayer.duration.inSeconds, 1)) * 100;
try { try {
if (event < const Duration(seconds: 3) || if (percentProgress < 80 ||
audioPlayerState.playlist.index == -1 || audioPlayerState.playlist.index == -1 ||
audioPlayerState.playlist.index == audioPlayerState.playlist.index ==
audioPlayerState.tracks.length - 1) { audioPlayerState.tracks.length - 1) {
return; return;
} }
final nextTrack = SpotubeMedia.fromMedia(audioPlayerState final nextTrack = SpotubeMedia.fromMedia(
.playlist.medias audioPlayerState.playlist.medias
.elementAt(audioPlayerState.playlist.index + 1)); .elementAt(audioPlayerState.playlist.index + 1),
);
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
return; return;

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart' hide Response; import 'package:dio/dio.dart' hide Response;
import 'package:dio/dio.dart' as dio_lib; 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/artist_simple.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/track.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/models/parser/range_headers.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.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/enums.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/service_utils.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 { class ServerPlaybackRoutes {
final Ref ref; final Ref ref;
@ -47,9 +63,8 @@ class ServerPlaybackRoutes {
var options = Options( var options = Options(
headers: { headers: {
...headers, ...headers,
"User-Agent": "user-agent": _randomUserAgent,
"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=3600",
"Cache-Control": "max-age=0",
"Connection": "keep-alive", "Connection": "keep-alive",
"host": Uri.parse(track.url).host, "host": Uri.parse(track.url).host,
}, },
@ -100,18 +115,35 @@ class ServerPlaybackRoutes {
); );
} }
final res = final res = await dio
await dio.get<Uint8List>(track.url, options: options).catchError( .get<Uint8List>(
(e, stack) async { track.url,
final sourcedTrack = await ref 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) .read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
.switchToAlternativeSources(); .switchToAlternativeSources();
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
return await dio.get<Uint8List>(sourcedTrack!.url, options: options); return await dio.get<Uint8List>(
}, sourcedTrack!.url,
options: options.copyWith(headers: {
...?options.headers,
"user-agent": _randomUserAgent,
}),
); );
});
final bytes = res.data; final bytes = res.data;

View File

@ -29,6 +29,19 @@ class SourcedTrackNotifier
return sourcedTrack; return sourcedTrack;
} }
Future<SourcedTrack?> refreshStreamingUrl() async {
if (arg == null) {
return null;
}
return await update((prev) async {
return await SourcedTrack.fetchFromTrack(
track: state.value!,
ref: ref,
);
});
}
Future<SourcedTrack?> switchToAlternativeSources() async { Future<SourcedTrack?> switchToAlternativeSources() async {
if (arg == null) { if (arg == null) {
return null; return null;

View File

@ -38,7 +38,7 @@ class AppLogger {
if (!kDebugMode) return; if (!kDebugMode) return;
logging.hierarchicalLoggingEnabled = true; logging.hierarchicalLoggingEnabled = true;
logging.Logger('YoutubeExplode.StreamsClient') logging.Logger('YoutubeExplode.StreamsClient')
..level = logging.Level.ALL ..level = logging.Level.SEVERE
..onRecord.listen( ..onRecord.listen(
(record) { (record) {
log.log( log.log(

View File

@ -50,7 +50,6 @@ class YoutubeSourcedTrack extends SourcedTrack {
ytClients: [ ytClients: [
YoutubeApiClient.android, YoutubeApiClient.android,
YoutubeApiClient.mweb, YoutubeApiClient.mweb,
YoutubeApiClient.safari,
], ],
); );
} }
@ -59,6 +58,23 @@ class YoutubeSourcedTrack extends SourcedTrack {
required Track track, required Track track,
required Ref ref, required Ref ref,
}) async { }) 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 database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable) final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(track.id!)) ..where((s) => s.trackId.equals(track.id!))
@ -94,7 +110,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
} }
final item = await youtubeClient.videos.get(cachedSource.sourceId); final item = await youtubeClient.videos.get(cachedSource.sourceId);
final manifest = await _getStreamManifest(cachedSource.sourceId); final manifest = await _getStreamManifest(cachedSource.sourceId);
return YoutubeSourcedTrack( final sourcedTrack = YoutubeSourcedTrack(
ref: ref, ref: ref,
siblings: [], siblings: [],
source: toSourceMap(manifest), source: toSourceMap(manifest),
@ -110,6 +126,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
), ),
track: track, track: track,
); );
AppLogger.log.i("${track.name}: ${sourcedTrack.url}");
return sourcedTrack;
} }
static SourceMap toSourceMap(StreamManifest manifest) { static SourceMap toSourceMap(StreamManifest manifest) {