mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
fix(playback): sponsor block skips and stutters in same position
This commit is contained in:
parent
f26503990c
commit
0d080b77b7
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -2,6 +2,7 @@
|
|||||||
"cmake.configureOnOpen": false,
|
"cmake.configureOnOpen": false,
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"acousticness",
|
"acousticness",
|
||||||
|
"Amoled",
|
||||||
"Buildless",
|
"Buildless",
|
||||||
"danceability",
|
"danceability",
|
||||||
"fuzzywuzzy",
|
"fuzzywuzzy",
|
||||||
|
@ -6,7 +6,7 @@ abstract class FakeData {
|
|||||||
static final Image image = Image()
|
static final Image image = Image()
|
||||||
..height = 1
|
..height = 1
|
||||||
..width = 1
|
..width = 1
|
||||||
..url = "url";
|
..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg";
|
||||||
|
|
||||||
static final Followers followers = Followers()
|
static final Followers followers = Followers()
|
||||||
..href = "text"
|
..href = "text"
|
||||||
|
@ -24,7 +24,6 @@ import 'package:spotube/models/logger.dart';
|
|||||||
import 'package:spotube/models/skip_segment.dart';
|
import 'package:spotube/models/skip_segment.dart';
|
||||||
import 'package:spotube/models/source_match.dart';
|
import 'package:spotube/models/source_match.dart';
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
|
||||||
import 'package:spotube/provider/connect/server.dart';
|
import 'package:spotube/provider/connect/server.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
132
lib/provider/proxy_playlist/player_listeners.dart
Normal file
132
lib/provider/proxy_playlist/player_listeners.dart
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/skip_segments.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
|
|
||||||
|
extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
|
||||||
|
StreamSubscription<String> subscribeToSourceChanges() =>
|
||||||
|
audioPlayer.activeSourceChangedStream.listen((event) {
|
||||||
|
try {
|
||||||
|
final newActiveTrack = mapSourcesToTracks([event]).firstOrNull;
|
||||||
|
|
||||||
|
if (newActiveTrack == null ||
|
||||||
|
newActiveTrack.id == state.activeTrack?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationService.addTrack(newActiveTrack);
|
||||||
|
discord.updatePresence(newActiveTrack);
|
||||||
|
state = state.copyWith(
|
||||||
|
active: state.tracks
|
||||||
|
.toList()
|
||||||
|
.indexWhere((element) => element.id == newActiveTrack.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
updatePalette();
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Catcher2.reportCheckedError(e, stackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
StreamSubscription subscribeToPercentCompletion() {
|
||||||
|
final isPreSearching = ObjectRef(false);
|
||||||
|
|
||||||
|
return audioPlayer.percentCompletedStream(2).listen((event) async {
|
||||||
|
if (isPreSearching.value ||
|
||||||
|
audioPlayer.currentSource == null ||
|
||||||
|
audioPlayer.nextSource == null ||
|
||||||
|
isPlayable(audioPlayer.nextSource!)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isPreSearching.value = true;
|
||||||
|
|
||||||
|
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
||||||
|
|
||||||
|
if (track != null) {
|
||||||
|
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
// Removing tracks that were not found to avoid queue interruption
|
||||||
|
if (e is TrackNotFoundError) {
|
||||||
|
final oldTrack =
|
||||||
|
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||||
|
await removeTrack(oldTrack!.id!);
|
||||||
|
}
|
||||||
|
Catcher2.reportCheckedError(e, stackTrace);
|
||||||
|
} finally {
|
||||||
|
isPreSearching.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToShuffleChanges() {
|
||||||
|
return audioPlayer.shuffledStream.listen((event) {
|
||||||
|
try {
|
||||||
|
final newlyOrderedTracks = mapSourcesToTracks(audioPlayer.sources);
|
||||||
|
|
||||||
|
final newActiveIndex = newlyOrderedTracks.indexWhere(
|
||||||
|
(element) => element.id == state.activeTrack?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newActiveIndex == -1) return;
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
tracks: newlyOrderedTracks.toSet(),
|
||||||
|
active: newActiveIndex,
|
||||||
|
);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Catcher2.reportCheckedError(e, stackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToSkipSponsor() {
|
||||||
|
return audioPlayer.positionStream.listen((position) async {
|
||||||
|
final currentSegments = await ref.read(segmentProvider.future);
|
||||||
|
|
||||||
|
if (currentSegments?.segments.isNotEmpty != true ||
|
||||||
|
position < const Duration(seconds: 3)) return;
|
||||||
|
|
||||||
|
for (final segment in currentSegments!.segments) {
|
||||||
|
final seconds = position.inSeconds;
|
||||||
|
|
||||||
|
if (seconds < segment.start || seconds >= segment.end) continue;
|
||||||
|
|
||||||
|
await audioPlayer.seek(Duration(seconds: segment.end + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToScrobbleChanged() {
|
||||||
|
String? lastScrobbled;
|
||||||
|
return audioPlayer.positionStream.listen((position) {
|
||||||
|
try {
|
||||||
|
final uid = state.activeTrack is LocalTrack
|
||||||
|
? (state.activeTrack as LocalTrack).path
|
||||||
|
: state.activeTrack?.id;
|
||||||
|
|
||||||
|
if (state.activeTrack == null ||
|
||||||
|
lastScrobbled == uid ||
|
||||||
|
position.inSeconds < 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrobbler.scrobble(state.activeTrack!);
|
||||||
|
lastScrobbled = uid;
|
||||||
|
} catch (e, stack) {
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToPlayerError() {
|
||||||
|
return audioPlayer.errorStream.listen((event) {});
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:catcher_2/catcher_2.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:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/models/skip_segment.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart';
|
import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/player_listeners.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
@ -26,34 +20,11 @@ import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||||
import 'package:spotube/provider/discord_provider.dart';
|
import 'package:spotube/provider/discord_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
|
|
||||||
/// Things implemented:
|
|
||||||
/// * [x] Sponsor-Block skip
|
|
||||||
/// * [x] Prefetch next track as [SourcedTrack] on 80% of current track
|
|
||||||
/// * [x] Mixed Queue containing both [SourcedTrack] and [LocalTrack]
|
|
||||||
/// * [x] Modification of the Queue
|
|
||||||
/// * [x] Add track at the end
|
|
||||||
/// * [x] Add track at the beginning
|
|
||||||
/// * [x] Remove track
|
|
||||||
/// * [x] Reorder track
|
|
||||||
/// * [x] Caching and loading of cache of tracks
|
|
||||||
/// * [x] Shuffling
|
|
||||||
/// * [x] loop => playlist, track, none
|
|
||||||
/// * [x] Alternative Track Source
|
|
||||||
/// * [x] Blacklisting of tracks and artist
|
|
||||||
///
|
|
||||||
/// Don'ts:
|
|
||||||
/// * It'll not have any proxy method for [SpotubeAudioPlayer]
|
|
||||||
/// * It'll not store any sort of player state e.g playing, paused, shuffled etc
|
|
||||||
/// * For that, use [SpotubeAudioPlayer]
|
|
||||||
|
|
||||||
class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||||
with NextFetcher {
|
with NextFetcher {
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
@ -74,162 +45,21 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
static AlwaysAliveRefreshable<ProxyPlaylistNotifier> get notifier =>
|
static AlwaysAliveRefreshable<ProxyPlaylistNotifier> get notifier =>
|
||||||
provider.notifier;
|
provider.notifier;
|
||||||
|
|
||||||
|
List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
ProxyPlaylistNotifier(this.ref) : super(ProxyPlaylist({}), "playlist") {
|
ProxyPlaylistNotifier(this.ref) : super(ProxyPlaylist({}), "playlist") {
|
||||||
() async {
|
AudioServices.create(ref, this).then(
|
||||||
notificationService = await AudioServices.create(ref, this);
|
(value) => notificationService = value,
|
||||||
|
);
|
||||||
|
|
||||||
// listeners state
|
_subscriptions = [
|
||||||
final currentSegments =
|
// These are subscription methods from player_listeners.dart
|
||||||
// using source as unique id because alternative track source support
|
subscribeToSourceChanges(),
|
||||||
ObjectRef<({String source, List<SkipSegment> segments})?>(null);
|
subscribeToPercentCompletion(),
|
||||||
final isPreSearching = ObjectRef(false);
|
subscribeToShuffleChanges(),
|
||||||
final isFetchingSegments = ObjectRef(false);
|
subscribeToSkipSponsor(),
|
||||||
|
subscribeToScrobbleChanged(),
|
||||||
audioPlayer.activeSourceChangedStream.listen((newActiveSource) async {
|
];
|
||||||
try {
|
|
||||||
final newActiveTrack =
|
|
||||||
mapSourcesToTracks([newActiveSource]).firstOrNull;
|
|
||||||
|
|
||||||
if (newActiveTrack == null ||
|
|
||||||
newActiveTrack.id == state.activeTrack?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationService.addTrack(newActiveTrack);
|
|
||||||
discord.updatePresence(newActiveTrack);
|
|
||||||
state = state.copyWith(
|
|
||||||
active: state.tracks
|
|
||||||
.toList()
|
|
||||||
.indexWhere((element) => element.id == newActiveTrack.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
updatePalette();
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audioPlayer.shuffledStream.listen((event) {
|
|
||||||
try {
|
|
||||||
final newlyOrderedTracks = mapSourcesToTracks(audioPlayer.sources);
|
|
||||||
|
|
||||||
final newActiveIndex = newlyOrderedTracks.indexWhere(
|
|
||||||
(element) => element.id == state.activeTrack?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newActiveIndex == -1) return;
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: newlyOrderedTracks.toSet(),
|
|
||||||
active: newActiveIndex,
|
|
||||||
);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listenTo2Percent(int percent) async {
|
|
||||||
if (isPreSearching.value ||
|
|
||||||
audioPlayer.currentSource == null ||
|
|
||||||
audioPlayer.nextSource == null ||
|
|
||||||
isPlayable(audioPlayer.nextSource!)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isPreSearching.value = true;
|
|
||||||
|
|
||||||
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
|
||||||
|
|
||||||
if (track != null) {
|
|
||||||
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
// Removing tracks that were not found to avoid queue interruption
|
|
||||||
if (e is TrackNotFoundError) {
|
|
||||||
final oldTrack =
|
|
||||||
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
|
||||||
await removeTrack(oldTrack!.id!);
|
|
||||||
}
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
} finally {
|
|
||||||
isPreSearching.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audioPlayer.percentCompletedStream(2).listen(listenTo2Percent);
|
|
||||||
|
|
||||||
audioPlayer.positionStream.listen((position) async {
|
|
||||||
if (state.activeTrack == null || state.activeTrack is LocalTrack) {
|
|
||||||
isFetchingSegments.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final isNotYTMode = state.activeTrack is! YoutubeSourcedTrack &&
|
|
||||||
(state.activeTrack is PipedSourcedTrack &&
|
|
||||||
preferences.searchMode == SearchMode.youtubeMusic);
|
|
||||||
|
|
||||||
if (isNotYTMode || !preferences.skipNonMusic) return;
|
|
||||||
|
|
||||||
final isNotSameSegmentId =
|
|
||||||
currentSegments.value?.source != audioPlayer.currentSource;
|
|
||||||
|
|
||||||
if (currentSegments.value == null ||
|
|
||||||
(isNotSameSegmentId && !isFetchingSegments.value)) {
|
|
||||||
isFetchingSegments.value = true;
|
|
||||||
try {
|
|
||||||
currentSegments.value = (
|
|
||||||
source: audioPlayer.currentSource!,
|
|
||||||
segments: await getAndCacheSkipSegments(
|
|
||||||
(state.activeTrack as SourcedTrack).sourceInfo.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (audioPlayer.currentSource != null) {
|
|
||||||
currentSegments.value = (
|
|
||||||
source: audioPlayer.currentSource!,
|
|
||||||
segments: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isFetchingSegments.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skipping in first 2 second breaks stream
|
|
||||||
if (currentSegments.value == null ||
|
|
||||||
currentSegments.value!.segments.isEmpty ||
|
|
||||||
position < const Duration(seconds: 3)) return;
|
|
||||||
|
|
||||||
for (final segment in currentSegments.value!.segments) {
|
|
||||||
if (position.inSeconds >= segment.start &&
|
|
||||||
position.inSeconds < segment.end) {
|
|
||||||
await audioPlayer.seek(Duration(seconds: segment.end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
String? lastScrobbled;
|
|
||||||
audioPlayer.positionStream.listen((position) {
|
|
||||||
try {
|
|
||||||
final uid = state.activeTrack is LocalTrack
|
|
||||||
? (state.activeTrack as LocalTrack).path
|
|
||||||
: state.activeTrack?.id;
|
|
||||||
|
|
||||||
if (state.activeTrack == null ||
|
|
||||||
lastScrobbled == uid ||
|
|
||||||
position.inSeconds < 30) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrobbler.scrobble(state.activeTrack!);
|
|
||||||
lastScrobbled = uid;
|
|
||||||
} catch (e, stack) {
|
|
||||||
Catcher2.reportCheckedError(e, stack);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SourcedTrack?> ensureSourcePlayable(String source) async {
|
Future<SourcedTrack?> ensureSourcePlayable(String source) async {
|
||||||
@ -283,8 +113,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Safely Remove playing tracks
|
|
||||||
|
|
||||||
Future<void> removeTrack(String trackId) async {
|
Future<void> removeTrack(String trackId) async {
|
||||||
final track =
|
final track =
|
||||||
state.tracks.firstWhereOrNull((element) => element.id == trackId);
|
state.tracks.firstWhereOrNull((element) => element.id == trackId);
|
||||||
@ -533,72 +361,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SkipSegment>> getAndCacheSkipSegments(String id) async {
|
|
||||||
if (!preferences.skipNonMusic ||
|
|
||||||
(preferences.audioSource == AudioSource.piped &&
|
|
||||||
preferences.searchMode == SearchMode.youtubeMusic)) return [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
final cached = await SkipSegment.box.get(id);
|
|
||||||
if (cached != null && cached.isNotEmpty) {
|
|
||||||
return List.castFrom<dynamic, SkipSegment>(
|
|
||||||
(cached as List)
|
|
||||||
.map(
|
|
||||||
(json) => SkipSegment.fromJson(
|
|
||||||
Map.castFrom<dynamic, dynamic, String, dynamic>(json),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final res = await get(Uri(
|
|
||||||
scheme: "https",
|
|
||||||
host: "sponsor.ajay.app",
|
|
||||||
path: "/api/skipSegments",
|
|
||||||
queryParameters: {
|
|
||||||
"videoID": id,
|
|
||||||
"category": [
|
|
||||||
'sponsor',
|
|
||||||
'selfpromo',
|
|
||||||
'interaction',
|
|
||||||
'intro',
|
|
||||||
'outro',
|
|
||||||
'music_offtopic'
|
|
||||||
],
|
|
||||||
"actionType": 'skip'
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
if (res.body == "Not Found") {
|
|
||||||
return List.castFrom<dynamic, SkipSegment>([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = jsonDecode(res.body) as List;
|
|
||||||
final segments = data.map((obj) {
|
|
||||||
final start = obj["segment"].first.toInt();
|
|
||||||
final end = obj["segment"].last.toInt();
|
|
||||||
return SkipSegment(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
getLogger('getSkipSegments').t(
|
|
||||||
"[SponsorBlock] successfully fetched skip segments for $id",
|
|
||||||
);
|
|
||||||
|
|
||||||
await SkipSegment.box.put(
|
|
||||||
id,
|
|
||||||
segments.map((e) => e.toJson()).toList(),
|
|
||||||
);
|
|
||||||
return List.castFrom<dynamic, SkipSegment>(segments);
|
|
||||||
} catch (e, stack) {
|
|
||||||
await SkipSegment.box.put(id, []);
|
|
||||||
Catcher2.reportCheckedError(e, stack);
|
|
||||||
return List.castFrom<dynamic, SkipSegment>([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
set state(state) {
|
set state(state) {
|
||||||
super.state = state;
|
super.state = state;
|
||||||
@ -631,4 +393,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
final json = state.toJson();
|
final json = state.toJson();
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (final subscription in _subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
110
lib/provider/proxy_playlist/skip_segments.dart
Normal file
110
lib/provider/proxy_playlist/skip_segments.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/models/skip_segment.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
|
class SourcedSegments {
|
||||||
|
final String source;
|
||||||
|
final List<SkipSegment> segments;
|
||||||
|
|
||||||
|
SourcedSegments({required this.source, required this.segments});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SkipSegment>> getAndCacheSkipSegments(String id) async {
|
||||||
|
try {
|
||||||
|
final cached = await SkipSegment.box.get(id) as List?;
|
||||||
|
if (cached != null && cached.isNotEmpty) {
|
||||||
|
return List.castFrom<dynamic, SkipSegment>(
|
||||||
|
cached
|
||||||
|
.map(
|
||||||
|
(json) => SkipSegment.fromJson(
|
||||||
|
Map.castFrom<dynamic, dynamic, String, dynamic>(json),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = await get(Uri(
|
||||||
|
scheme: "https",
|
||||||
|
host: "sponsor.ajay.app",
|
||||||
|
path: "/api/skipSegments",
|
||||||
|
queryParameters: {
|
||||||
|
"videoID": id,
|
||||||
|
"category": [
|
||||||
|
'sponsor',
|
||||||
|
'selfpromo',
|
||||||
|
'interaction',
|
||||||
|
'intro',
|
||||||
|
'outro',
|
||||||
|
'music_offtopic'
|
||||||
|
],
|
||||||
|
"actionType": 'skip'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
if (res.body == "Not Found") {
|
||||||
|
return List.castFrom<dynamic, SkipSegment>([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(res.body) as List;
|
||||||
|
final segments = data.map((obj) {
|
||||||
|
final start = obj["segment"].first.toInt();
|
||||||
|
final end = obj["segment"].last.toInt();
|
||||||
|
return SkipSegment(start, end);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await SkipSegment.box.put(
|
||||||
|
id,
|
||||||
|
segments.map((e) => e.toJson()).toList(),
|
||||||
|
);
|
||||||
|
return List.castFrom<dynamic, SkipSegment>(segments);
|
||||||
|
} catch (e, stack) {
|
||||||
|
await SkipSegment.box.put(id, []);
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
return List.castFrom<dynamic, SkipSegment>([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final segmentProvider = FutureProvider<SourcedSegments?>(
|
||||||
|
(ref) async {
|
||||||
|
final track = ref.watch(
|
||||||
|
ProxyPlaylistNotifier.provider.select((s) => s.activeTrack),
|
||||||
|
);
|
||||||
|
if (track == null) return null;
|
||||||
|
|
||||||
|
if (track is LocalTrack || track is! SourcedTrack) return null;
|
||||||
|
|
||||||
|
final skipNonMusic = ref.watch(
|
||||||
|
userPreferencesProvider.select(
|
||||||
|
(s) {
|
||||||
|
final isPipedYTMusicMode = s.audioSource == AudioSource.piped &&
|
||||||
|
s.searchMode == SearchMode.youtubeMusic;
|
||||||
|
|
||||||
|
return s.skipNonMusic && !isPipedYTMusicMode;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!skipNonMusic) {
|
||||||
|
return SourcedSegments(
|
||||||
|
segments: [],
|
||||||
|
source: track.sourceInfo.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final segments = await getAndCacheSkipSegments(track.sourceInfo.id);
|
||||||
|
|
||||||
|
return SourcedSegments(
|
||||||
|
source: track.sourceInfo.id,
|
||||||
|
segments: segments,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
@ -146,4 +146,6 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
|
|||||||
|
|
||||||
Stream<AudioDevice> get selectedDeviceStream =>
|
Stream<AudioDevice> get selectedDeviceStream =>
|
||||||
_mkPlayer.stream.audioDevice.asBroadcastStream();
|
_mkPlayer.stream.audioDevice.asBroadcastStream();
|
||||||
|
|
||||||
|
Stream<String> get errorStream => _mkPlayer.stream.error;
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
};
|
};
|
||||||
} on HttpClientClosedException catch (_) {
|
} on HttpClientClosedException catch (_) {
|
||||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||||
|
} on VideoUnplayableException catch (_) {
|
||||||
|
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is DioException || e is ClientException || e is SocketException) {
|
if (e is DioException || e is ClientException || e is SocketException) {
|
||||||
if (preferences.audioSource == AudioSource.jiosaavn) {
|
if (preferences.audioSource == AudioSource.jiosaavn) {
|
||||||
|
Loading…
Reference in New Issue
Block a user