From 6ced0a0fad06f9f431636ca0fe5dae83eafe33ce Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 25 Aug 2023 18:05:18 +0600 Subject: [PATCH] fix: always fetching SponsorBlock if no segments found & download failing --- .../dialogs/confirm_download_dialog.dart | 6 +- .../shared/track_table/tracks_table_view.dart | 18 +++-- lib/models/spotube_track.dart | 8 ++- lib/provider/download_manager_provider.dart | 2 +- .../proxy_playlist_provider.dart | 69 ++++++++++++------- lib/provider/youtube_provider.dart | 10 --- .../audio_services/windows_audio_service.dart | 3 +- 7 files changed, 67 insertions(+), 49 deletions(-) diff --git a/lib/components/shared/dialogs/confirm_download_dialog.dart b/lib/components/shared/dialogs/confirm_download_dialog.dart index 0413260c2..c371e8032 100644 --- a/lib/components/shared/dialogs/confirm_download_dialog.dart +++ b/lib/components/shared/dialogs/confirm_download_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; class ConfirmDownloadDialog extends StatelessWidget { @@ -24,8 +25,9 @@ class ConfirmDownloadDialog extends StatelessWidget { ], ), ), - content: Padding( + content: Container( padding: const EdgeInsets.all(15), + constraints: BoxConstraints(maxWidth: Breakpoints.sm), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -87,7 +89,7 @@ class BulletPoint extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("●"), + const Text("\u2022"), const SizedBox(width: 5), Flexible(child: Text(text)), ], diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index d412dd364..58d662f45 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -20,6 +20,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/utils/service_utils.dart'; final trackCollectionSortState = @@ -55,7 +56,9 @@ class TracksTableView extends HookConsumerWidget { final playback = ref.watch(ProxyPlaylistNotifier.notifier); ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); - TextStyle tableHeadStyle = + final apiType = + ref.watch(userPreferencesProvider.select((s) => s.youtubeApiType)); + final tableHeadStyle = const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); final selected = useState>([]); @@ -188,12 +191,13 @@ class TracksTableView extends HookConsumerWidget { switch (action) { case "download": { - final confirmed = await showDialog( - context: context, - builder: (context) { - return const ConfirmDownloadDialog(); - }, - ); + final confirmed = apiType == YoutubeApiType.piped || + await showDialog( + context: context, + builder: (context) { + return const ConfirmDownloadDialog(); + }, + ); if (confirmed != true) return; await downloader .batchAddToQueue(selectedTracks.toList()); diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index 268a273ec..6ef240df2 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -14,6 +14,12 @@ final officialMusicRegex = RegExp( caseSensitive: false, ); +class TrackNotFoundException implements Exception { + factory TrackNotFoundException(Track track) { + throw Exception("Failed to find any results for ${track.name}"); + } +} + class SpotubeTrack extends Track { final YoutubeVideoInfo ytTrack; final String ytUri; @@ -157,7 +163,7 @@ class SpotubeTrack extends Track { } else { siblings = await fetchSiblings(track, client); if (siblings.isEmpty) { - throw Exception("Failed to find any results for ${track.name}"); + throw TrackNotFoundException(track); } (ytVideo, ytStreamUrl) = await client.video(siblings.first.id, siblings.first.searchMode); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index db4430826..b5c4a0e75 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -85,7 +85,7 @@ class DownloadManagerProvider extends ChangeNotifier { final Ref ref; - YoutubeEndpoints get yt => ref.read(downloadYoutubeProvider); + YoutubeEndpoints get yt => ref.read(youtubeProvider); String get downloadDirectory => ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index b5d42fb2e..4879867b8 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:catcher/catcher.dart'; import 'package:collection/collection.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:palette_generator/palette_generator.dart'; @@ -68,7 +69,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier () async { notificationService = await AudioServices.create(ref, this); - ({String source, List segments})? currentSegments; + // listeners state + final currentSegments = + // using source as unique id because alternative track source support + ObjectRef<({String source, List segments})?>(null); + final isPreSearching = ObjectRef(false); + final isFetchingSegments = ObjectRef(false); audioPlayer.activeSourceChangedStream.listen((newActiveSource) async { try { @@ -112,16 +118,14 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } }); - bool isPreSearching = false; - listenTo2Percent(int percent) async { - if (isPreSearching || + if (isPreSearching.value || audioPlayer.currentSource == null || audioPlayer.nextSource == null || isPlayable(audioPlayer.nextSource!)) return; try { - isPreSearching = true; + isPreSearching.value = true; final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; @@ -138,51 +142,64 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ); } } catch (e, stackTrace) { + // Removing tracks that were not found to avoid queue interruption + // TODO: Add a flag to enable/disable skip not found tracks + if (e is TrackNotFoundException) { + final oldTrack = + mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; + await removeTrack(oldTrack!.id!); + } Catcher.reportCheckedError(e, stackTrace); } finally { - isPreSearching = false; + isPreSearching.value = false; } } audioPlayer.percentCompletedStream(2).listen(listenTo2Percent); - bool isFetchingSegments = false; - audioPlayer.positionStream.listen((position) async { + if (state.activeTrack == null || state.activeTrack is LocalTrack) { + isFetchingSegments.value = false; + return; + } try { - if (state.activeTrack == null || state.activeTrack is LocalTrack) { - isFetchingSegments = false; - return; - } - // skipping in very first second breaks stream - if ((preferences.youtubeApiType == YoutubeApiType.piped && - preferences.searchMode == SearchMode.youtubeMusic) || - !preferences.skipNonMusic) return; + final isYTMusicMode = + preferences.youtubeApiType == YoutubeApiType.piped && + preferences.searchMode == SearchMode.youtubeMusic; + + if (isYTMusicMode || !preferences.skipNonMusic) return; - final notSameSegmentId = - currentSegments?.source != audioPlayer.currentSource; + final isNotSameSegmentId = + currentSegments.value?.source != audioPlayer.currentSource; - if (currentSegments == null || - (notSameSegmentId && !isFetchingSegments)) { - isFetchingSegments = true; + if (currentSegments.value == null || + (isNotSameSegmentId && !isFetchingSegments.value)) { + isFetchingSegments.value = true; try { - currentSegments = ( + currentSegments.value = ( source: audioPlayer.currentSource!, segments: await getAndCacheSkipSegments( (state.activeTrack as SpotubeTrack).ytTrack.id, ), ); + } catch (e) { + currentSegments.value = ( + source: audioPlayer.currentSource!, + segments: [], + ); } finally { - isFetchingSegments = false; + isFetchingSegments.value = false; } } - final (source: _, :segments) = currentSegments!; + final (source: _, :segments) = currentSegments.value!; + + // skipping in first 2 second breaks stream if (segments.isEmpty || position < const Duration(seconds: 3)) return; for (final segment in segments) { - if ((position.inSeconds >= segment.start && - position.inSeconds < segment.end)) { + if (position.inSeconds >= segment.start && + position.inSeconds < segment.end) { await audioPlayer.seek(Duration(seconds: segment.end)); } } diff --git a/lib/provider/youtube_provider.dart b/lib/provider/youtube_provider.dart index 20b5ba2bb..0e7b7d0ea 100644 --- a/lib/provider/youtube_provider.dart +++ b/lib/provider/youtube_provider.dart @@ -6,13 +6,3 @@ final youtubeProvider = Provider((ref) { final preferences = ref.watch(userPreferencesProvider); return YoutubeEndpoints(preferences); }); - -// this provider overrides the API provider to use piped.video for downloading -final downloadYoutubeProvider = Provider((ref) { - final preferences = ref.watch(userPreferencesProvider); - return YoutubeEndpoints( - preferences.copyWith( - youtubeApiType: YoutubeApiType.piped, - ), - ); -}); diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index 0cd8f9bbb..4481140b8 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:smtc_windows/smtc_windows.dart' - if (dart.library.html) 'package:spotube/services/audio_services/smtc_windows_web.dart'; +import 'package:smtc_windows/smtc_windows.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart';