fix: always fetching SponsorBlock if no segments found & download failing

This commit is contained in:
Kingkor Roy Tirtho 2023-08-25 18:05:18 +06:00
parent 33c7b64f06
commit 6ced0a0fad
7 changed files with 67 additions and 49 deletions

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
class ConfirmDownloadDialog extends StatelessWidget { class ConfirmDownloadDialog extends StatelessWidget {
@ -24,8 +25,9 @@ class ConfirmDownloadDialog extends StatelessWidget {
], ],
), ),
), ),
content: Padding( content: Container(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
constraints: BoxConstraints(maxWidth: Breakpoints.sm),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -87,7 +89,7 @@ class BulletPoint extends StatelessWidget {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text(""), const Text("\u2022"),
const SizedBox(width: 5), const SizedBox(width: 5),
Flexible(child: Text(text)), Flexible(child: Text(text)),
], ],

View File

@ -20,6 +20,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_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'; import 'package:spotube/utils/service_utils.dart';
final trackCollectionSortState = final trackCollectionSortState =
@ -55,7 +56,9 @@ class TracksTableView extends HookConsumerWidget {
final playback = ref.watch(ProxyPlaylistNotifier.notifier); final playback = ref.watch(ProxyPlaylistNotifier.notifier);
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier); 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); const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
final selected = useState<List<String>>([]); final selected = useState<List<String>>([]);
@ -188,12 +191,13 @@ class TracksTableView extends HookConsumerWidget {
switch (action) { switch (action) {
case "download": case "download":
{ {
final confirmed = await showDialog( final confirmed = apiType == YoutubeApiType.piped ||
context: context, await showDialog(
builder: (context) { context: context,
return const ConfirmDownloadDialog(); builder: (context) {
}, return const ConfirmDownloadDialog();
); },
);
if (confirmed != true) return; if (confirmed != true) return;
await downloader await downloader
.batchAddToQueue(selectedTracks.toList()); .batchAddToQueue(selectedTracks.toList());

View File

@ -14,6 +14,12 @@ final officialMusicRegex = RegExp(
caseSensitive: false, caseSensitive: false,
); );
class TrackNotFoundException implements Exception {
factory TrackNotFoundException(Track track) {
throw Exception("Failed to find any results for ${track.name}");
}
}
class SpotubeTrack extends Track { class SpotubeTrack extends Track {
final YoutubeVideoInfo ytTrack; final YoutubeVideoInfo ytTrack;
final String ytUri; final String ytUri;
@ -157,7 +163,7 @@ class SpotubeTrack extends Track {
} else { } else {
siblings = await fetchSiblings(track, client); siblings = await fetchSiblings(track, client);
if (siblings.isEmpty) { if (siblings.isEmpty) {
throw Exception("Failed to find any results for ${track.name}"); throw TrackNotFoundException(track);
} }
(ytVideo, ytStreamUrl) = (ytVideo, ytStreamUrl) =
await client.video(siblings.first.id, siblings.first.searchMode); await client.video(siblings.first.id, siblings.first.searchMode);

View File

@ -85,7 +85,7 @@ class DownloadManagerProvider extends ChangeNotifier {
final Ref<DownloadManagerProvider> ref; final Ref<DownloadManagerProvider> ref;
YoutubeEndpoints get yt => ref.read(downloadYoutubeProvider); YoutubeEndpoints get yt => ref.read(youtubeProvider);
String get downloadDirectory => String get downloadDirectory =>
ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); ref.read(userPreferencesProvider.select((s) => s.downloadLocation));

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:catcher/catcher.dart'; import 'package:catcher/catcher.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
@ -68,7 +69,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
() async { () async {
notificationService = await AudioServices.create(ref, this); notificationService = await AudioServices.create(ref, this);
({String source, List<SkipSegment> segments})? currentSegments; // listeners state
final currentSegments =
// using source as unique id because alternative track source support
ObjectRef<({String source, List<SkipSegment> segments})?>(null);
final isPreSearching = ObjectRef(false);
final isFetchingSegments = ObjectRef(false);
audioPlayer.activeSourceChangedStream.listen((newActiveSource) async { audioPlayer.activeSourceChangedStream.listen((newActiveSource) async {
try { try {
@ -112,16 +118,14 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
} }
}); });
bool isPreSearching = false;
listenTo2Percent(int percent) async { listenTo2Percent(int percent) async {
if (isPreSearching || if (isPreSearching.value ||
audioPlayer.currentSource == null || audioPlayer.currentSource == null ||
audioPlayer.nextSource == null || audioPlayer.nextSource == null ||
isPlayable(audioPlayer.nextSource!)) return; isPlayable(audioPlayer.nextSource!)) return;
try { try {
isPreSearching = true; isPreSearching.value = true;
final oldTrack = final oldTrack =
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
@ -138,51 +142,64 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
); );
} }
} catch (e, stackTrace) { } 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); Catcher.reportCheckedError(e, stackTrace);
} finally { } finally {
isPreSearching = false; isPreSearching.value = false;
} }
} }
audioPlayer.percentCompletedStream(2).listen(listenTo2Percent); audioPlayer.percentCompletedStream(2).listen(listenTo2Percent);
bool isFetchingSegments = false;
audioPlayer.positionStream.listen((position) async { audioPlayer.positionStream.listen((position) async {
if (state.activeTrack == null || state.activeTrack is LocalTrack) {
isFetchingSegments.value = false;
return;
}
try { try {
if (state.activeTrack == null || state.activeTrack is LocalTrack) { final isYTMusicMode =
isFetchingSegments = false; preferences.youtubeApiType == YoutubeApiType.piped &&
return; preferences.searchMode == SearchMode.youtubeMusic;
}
// skipping in very first second breaks stream
if ((preferences.youtubeApiType == YoutubeApiType.piped &&
preferences.searchMode == SearchMode.youtubeMusic) ||
!preferences.skipNonMusic) return;
final notSameSegmentId = if (isYTMusicMode || !preferences.skipNonMusic) return;
currentSegments?.source != audioPlayer.currentSource;
if (currentSegments == null || final isNotSameSegmentId =
(notSameSegmentId && !isFetchingSegments)) { currentSegments.value?.source != audioPlayer.currentSource;
isFetchingSegments = true;
if (currentSegments.value == null ||
(isNotSameSegmentId && !isFetchingSegments.value)) {
isFetchingSegments.value = true;
try { try {
currentSegments = ( currentSegments.value = (
source: audioPlayer.currentSource!, source: audioPlayer.currentSource!,
segments: await getAndCacheSkipSegments( segments: await getAndCacheSkipSegments(
(state.activeTrack as SpotubeTrack).ytTrack.id, (state.activeTrack as SpotubeTrack).ytTrack.id,
), ),
); );
} catch (e) {
currentSegments.value = (
source: audioPlayer.currentSource!,
segments: [],
);
} finally { } 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; if (segments.isEmpty || position < const Duration(seconds: 3)) return;
for (final segment in segments) { for (final segment in segments) {
if ((position.inSeconds >= segment.start && if (position.inSeconds >= segment.start &&
position.inSeconds < segment.end)) { position.inSeconds < segment.end) {
await audioPlayer.seek(Duration(seconds: segment.end)); await audioPlayer.seek(Duration(seconds: segment.end));
} }
} }

View File

@ -6,13 +6,3 @@ final youtubeProvider = Provider<YoutubeEndpoints>((ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
return YoutubeEndpoints(preferences); return YoutubeEndpoints(preferences);
}); });
// this provider overrides the API provider to use piped.video for downloading
final downloadYoutubeProvider = Provider<YoutubeEndpoints>((ref) {
final preferences = ref.watch(userPreferencesProvider);
return YoutubeEndpoints(
preferences.copyWith(
youtubeApiType: YoutubeApiType.piped,
),
);
});

View File

@ -1,8 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:smtc_windows/smtc_windows.dart' import 'package:smtc_windows/smtc_windows.dart';
if (dart.library.html) 'package:spotube/services/audio_services/smtc_windows_web.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';