feat: use catcher to handle exceptions

This commit is contained in:
Kingkor Roy Tirtho 2023-02-01 18:26:17 +06:00
parent 013aac4543
commit 84d94b05bc
23 changed files with 532 additions and 523 deletions

View File

@ -20,33 +20,28 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
@override @override
invoke(intent) async { invoke(intent) async {
try { if (PlayerControls.focusNode.canRequestFocus) {
if (PlayerControls.focusNode.canRequestFocus) { PlayerControls.focusNode.requestFocus();
PlayerControls.focusNode.requestFocus();
}
final playback = intent.ref.read(playbackProvider);
if (playback.track == null) {
return null;
} else if (playback.track != null &&
playback.currentDuration == Duration.zero &&
await playback.player.getCurrentPosition() == Duration.zero) {
if (playback.track!.ytUri.startsWith("http")) {
final track = Track.fromJson(playback.track!.toJson());
playback.track = null;
await playback.play(track);
} else {
final track = playback.track;
playback.track = null;
await playback.play(track!);
}
} else {
await playback.togglePlayPause();
}
return null;
} catch (e, stack) {
logger.e("useTogglePlayPause", e, stack);
return null;
} }
final playback = intent.ref.read(playbackProvider);
if (playback.track == null) {
return null;
} else if (playback.track != null &&
playback.currentDuration == Duration.zero &&
await playback.player.getCurrentPosition() == Duration.zero) {
if (playback.track!.ytUri.startsWith("http")) {
final track = Track.fromJson(playback.track!.toJson());
playback.track = null;
await playback.play(track);
} else {
final track = playback.track;
playback.track = null;
await playback.play(track!);
}
} else {
await playback.togglePlayPause();
}
return null;
} }
} }

View File

@ -1,3 +1,4 @@
import 'package:catcher/catcher.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotify/spotify.dart' hide Search;
@ -19,7 +20,7 @@ import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = Catcher.navigatorKey;
final shellRouteNavigatorKey = GlobalKey<NavigatorState>(); final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter( final router = GoRouter(
navigatorKey: rootNavigatorKey, navigatorKey: rootNavigatorKey,

View File

@ -60,5 +60,5 @@ abstract class SpotubeIcons {
static const info = FeatherIcons.info; static const info = FeatherIcons.info;
static const userRemove = FeatherIcons.userX; static const userRemove = FeatherIcons.userX;
static const close = FeatherIcons.x; static const close = FeatherIcons.x;
static const minimize = FeatherIcons.minimize2; static const minimize = FeatherIcons.chevronDown;
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:catcher/catcher.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -98,12 +99,11 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {
} on FfiException catch (e) { } on FfiException catch (e) {
if (e.message == "NoTag: reader does not contain an id3 tag") { if (e.message == "NoTag: reader does not contain an id3 tag") {
getLogger(FutureProvider<List<Track>>) getLogger(FutureProvider<List<Track>>)
.w("[Fetching metadata]", e.message); .v("[Fetching metadata]", e.message);
} }
return {}; return {};
} on Exception catch (e, stack) { } catch (e, stack) {
getLogger(FutureProvider<List<Track>>) Catcher.reportCheckedError(e, stack);
.e("[Fetching metadata]", e, stack);
return {}; return {};
} }
}, },
@ -124,7 +124,7 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {
return tracks; return tracks;
} catch (e, stack) { } catch (e, stack) {
getLogger(FutureProvider).e("[LocalTracksProvider]", e, stack); Catcher.reportCheckedError(e, stack);
return []; return [];
} }
}); });

View File

@ -199,15 +199,7 @@ class PlayerControls extends HookConsumerWidget {
SpotubeIcons.stop, SpotubeIcons.stop,
color: iconColor, color: iconColor,
), ),
onPressed: playback.track != null onPressed: playback.track != null ? playback.stop : null,
? () async {
try {
await playback.stop();
} catch (e, stack) {
logger.e("onStop", e, stack);
}
}
: null,
), ),
PlatformIconButton( PlatformIconButton(
tooltip: tooltip:

View File

@ -145,14 +145,10 @@ class BottomPlayer extends HookConsumerWidget {
volume.value = v; volume.value = v;
}, },
onChangeEnd: (value) async { onChangeEnd: (value) async {
try { // You don't really need to know why but this
// You don't really need to know why but this // way it works only
// way it works only await playback.setVolume(value);
await playback.setVolume(value); await playback.setVolume(value);
await playback.setVolume(value);
} catch (e, stack) {
logger.e("onChange", e, stack);
}
}, },
), ),
); );

View File

@ -10,7 +10,6 @@ import 'package:audio_service_web/audio_service_web.dart';
import 'package:audio_session/audio_session_web.dart'; import 'package:audio_session/audio_session_web.dart';
import 'package:audioplayers_web/audioplayers_web.dart'; import 'package:audioplayers_web/audioplayers_web.dart';
import 'package:file_picker/_internal/file_picker_web.dart'; import 'package:file_picker/_internal/file_picker_web.dart';
import 'package:package_info_plus_web/package_info_plus_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:url_launcher_web/url_launcher_web.dart'; import 'package:url_launcher_web/url_launcher_web.dart';
@ -22,7 +21,6 @@ void registerPlugins(Registrar registrar) {
AudioSessionWeb.registerWith(registrar); AudioSessionWeb.registerWith(registrar);
AudioplayersPlugin.registerWith(registrar); AudioplayersPlugin.registerWith(registrar);
FilePickerWeb.registerWith(registrar); FilePickerWeb.registerWith(registrar);
PackageInfoPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar); SharedPreferencesPlugin.registerWith(registrar);
UrlLauncherPlugin.registerWith(registrar); UrlLauncherPlugin.registerWith(registrar);
registrar.registerMessageHandler(); registrar.registerMessageHandler();

View File

@ -1,31 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playback_provider.dart'; import 'package:spotube/provider/playback_provider.dart';
final logger = getLogger("PlaybackHook");
Future<void> Function() useNextTrack(WidgetRef ref) { Future<void> Function() useNextTrack(WidgetRef ref) {
return () async { return () async {
try { final playback = ref.read(playbackProvider);
final playback = ref.read(playbackProvider); await playback.player.pause();
await playback.player.pause(); await playback.player.seek(Duration.zero);
await playback.player.seek(Duration.zero); playback.seekForward();
playback.seekForward();
} catch (e, stack) {
logger.e("useNextTrack", e, stack);
}
}; };
} }
Future<void> Function() usePreviousTrack(WidgetRef ref) { Future<void> Function() usePreviousTrack(WidgetRef ref) {
return () async { return () async {
try { final playback = ref.read(playbackProvider);
final playback = ref.read(playbackProvider); await playback.player.pause();
await playback.player.pause(); await playback.player.seek(Duration.zero);
await playback.player.seek(Duration.zero); playback.seekBackward();
playback.seekBackward();
} catch (e, stack) {
logger.e("onPrevious", e, stack);
}
}; };
} }

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:catcher/catcher.dart';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -61,82 +62,98 @@ void main() async {
}); });
} }
MobileAudioService? audioServiceHandler; MobileAudioService? audioServiceHandler;
runApp(
Builder(
builder: (context) {
return ProviderScope(
overrides: [
playbackProvider.overrideWith(
(ref) {
final youtube = ref.watch(youtubeProvider);
final player = ref.watch(audioPlayerProvider);
final playback = Playback( Catcher(
player: player, debugConfig: CatcherOptions(
youtube: youtube, SilentReportMode(),
ref: ref, [
); ConsoleHandler(
enableDeviceParameters: false,
if (audioServiceHandler == null) { enableApplicationParameters: false,
AudioService.init( ),
builder: () => MobileAudioService(playback), FileHandler(await getLogsPath(), printLogs: false),
config: const AudioServiceConfig( SnackbarHandler(const Duration(seconds: 5)),
androidNotificationChannelId: 'com.krtirtho.Spotube', ],
androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: true,
),
).then(
(value) {
playback.mobileAudioService = value;
audioServiceHandler = value;
},
);
}
return playback;
},
),
downloaderProvider.overrideWith(
(ref) {
return Downloader(
ref,
queueInstance,
yt: ref.watch(youtubeProvider),
downloadPath: ref.watch(
userPreferencesProvider.select(
(s) => s.downloadLocation,
),
),
onFileExists: (track) {
final logger = getLogger(Downloader);
try {
logger.v(
"[onFileExists] download confirmation for ${track.name}",
);
return showPlatformAlertDialog<bool>(
context,
builder: (_) => ReplaceDownloadedDialog(track: track),
).then((s) => s ?? false);
} catch (e, stack) {
logger.e(
"onFileExists",
e,
stack,
);
return false;
}
},
);
},
)
],
child: QueryBowlScope(
bowl: bowl,
child: const Spotube(),
),
);
},
), ),
releaseConfig: CatcherOptions(SilentReportMode(), [
FileHandler(await getLogsPath(), printLogs: false),
]),
runAppFunction: () {
runApp(
Builder(
builder: (context) {
return ProviderScope(
overrides: [
playbackProvider.overrideWith(
(ref) {
final youtube = ref.watch(youtubeProvider);
final player = ref.watch(audioPlayerProvider);
final playback = Playback(
player: player,
youtube: youtube,
ref: ref,
);
if (audioServiceHandler == null) {
AudioService.init(
builder: () => MobileAudioService(playback),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: true,
),
).then(
(value) {
playback.mobileAudioService = value;
audioServiceHandler = value;
},
);
}
return playback;
},
),
downloaderProvider.overrideWith(
(ref) {
return Downloader(
ref,
queueInstance,
yt: ref.watch(youtubeProvider),
downloadPath: ref.watch(
userPreferencesProvider.select(
(s) => s.downloadLocation,
),
),
onFileExists: (track) {
final logger = getLogger(Downloader);
try {
logger.v(
"[onFileExists] download confirmation for ${track.name}",
);
return showPlatformAlertDialog<bool>(
context,
builder: (_) =>
ReplaceDownloadedDialog(track: track),
).then((s) => s ?? false);
} catch (e, stack) {
Catcher.reportCheckedError(e, stack);
return false;
}
},
);
},
)
],
child: QueryBowlScope(
bowl: bowl,
child: const Spotube(),
),
);
},
),
);
},
); );
} }

View File

@ -13,6 +13,22 @@ SpotubeLogger getLogger<T>(T owner) {
return _loggerFactory; return _loggerFactory;
} }
Future<File> getLogsPath() async {
String dir = (await getApplicationDocumentsDirectory()).path;
if (kIsAndroid) {
dir = (await getExternalStorageDirectory())?.path ?? "";
}
if (kIsMacOS) {
dir = path.join((await getLibraryDirectory()).path, "Logs");
}
final file = File(path.join(dir, ".spotube_logs"));
if (!await file.exists()) {
await file.create();
}
return file;
}
class SpotubeLogger extends Logger { class SpotubeLogger extends Logger {
String? owner; String? owner;
SpotubeLogger([this.owner]) : super(filter: _SpotubeLogFilter()); SpotubeLogger([this.owner]) : super(filter: _SpotubeLogFilter());

View File

@ -203,12 +203,6 @@ class ArtistPage extends HookConsumerWidget {
.queryKey, .queryKey,
) )
?.refetch(); ?.refetch();
} catch (e, stack) {
logger.e(
"FollowButton.onPressed",
e,
stack,
);
} finally { } finally {
QueryBowl.of(context) QueryBowl.of(context)
.refetchQueries([ .refetchQueries([

View File

@ -134,12 +134,8 @@ class Downloader with ChangeNotifier {
logger.v( logger.v(
"[addToQueue] Writing metadata to ${file.path} is successful", "[addToQueue] Writing metadata to ${file.path} is successful",
); );
} catch (e, stack) { } catch (e) {
logger.e( logger.v("[addToQueue] Failed download of ${file.path}", e);
"[addToQueue] Failed download of ${file.path}",
e,
stack,
);
rethrow; rethrow;
} finally { } finally {
currentlyRunning--; currentlyRunning--;

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:catcher/catcher.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -198,75 +199,67 @@ class Playback extends PersistedChangeNotifier {
} }
Future<void> playPlaylist(CurrentPlaylist playlist, [int index = 0]) async { Future<void> playPlaylist(CurrentPlaylist playlist, [int index = 0]) async {
try { if (index < 0 || index > playlist.tracks.length - 1) return;
if (index < 0 || index > playlist.tracks.length - 1) return; if (isPlaying || status == PlaybackStatus.playing) await stop();
if (isPlaying || status == PlaybackStatus.playing) await stop(); this.playlist = blacklistNotifier.filterPlaylist(playlist);
this.playlist = blacklistNotifier.filterPlaylist(playlist); mobileAudioService?.session?.setActive(true);
mobileAudioService?.session?.setActive(true); final played = this.playlist!.tracks[index];
final played = this.playlist!.tracks[index]; status = PlaybackStatus.loading;
status = PlaybackStatus.loading; notifyListeners();
notifyListeners(); await play(played).then((_) {
await play(played).then((_) { int i = this
int i = this .playlist!
.playlist! .tracks
.tracks .indexWhere((element) => element.id == played.id);
.indexWhere((element) => element.id == played.id); if (index == -1) return;
if (index == -1) return; this.playlist!.tracks[i] = track!;
this.playlist!.tracks[i] = track!; });
});
} catch (e) {
_logger.e("[playPlaylist] $e");
}
} }
// player methods // player methods
Future<void> play(Track track, {AudioOnlyStreamInfo? manifest}) async { Future<void> play(Track track, {AudioOnlyStreamInfo? manifest}) async {
_logger.v("[Track Playing] ${track.name} - ${track.id}"); _logger.v("[Track Playing] ${track.name} - ${track.id}");
try { // the track is already playing so no need to change that
// the track is already playing so no need to change that if (track.id == this.track?.id) return;
if (track.id == this.track?.id) return; if (status != PlaybackStatus.loading) {
if (status != PlaybackStatus.loading) { status = PlaybackStatus.loading;
status = PlaybackStatus.loading;
notifyListeners();
}
_siblingYtVideos = [];
// the track is not a SpotubeTrack so turning it to one
if (track is! SpotubeTrack) {
final s = await toSpotubeTrack(track);
track = s.item1;
manifest = s.item2;
}
final tag = MediaItem(
id: track.id!,
title: track.name!,
album: track.album?.name,
artist: TypeConversionUtils.artists_X_String(
track.artists ?? <ArtistSimple>[]),
artUri: Uri.parse(
TypeConversionUtils.image_X_UrlString(
track.album?.images,
placeholder: ImagePlaceholder.online,
),
),
duration: track.ytTrack.duration,
);
mobileAudioService?.addItem(tag);
_logger.v("[Track Direct Source] - ${(track).ytUri}");
this.track = track;
notifyListeners(); notifyListeners();
updatePersistence();
await player.play(
track.ytUri.startsWith("http")
? await getAppropriateSource(track, manifest)
: DeviceFileSource(track.ytUri),
);
status = PlaybackStatus.playing;
notifyListeners();
} catch (e, stack) {
_logger.e("play", e, stack);
} }
_siblingYtVideos = [];
// the track is not a SpotubeTrack so turning it to one
if (track is! SpotubeTrack) {
final s = await toSpotubeTrack(track);
track = s.item1;
manifest = s.item2;
}
final tag = MediaItem(
id: track.id!,
title: track.name!,
album: track.album?.name,
artist: TypeConversionUtils.artists_X_String(
track.artists ?? <ArtistSimple>[]),
artUri: Uri.parse(
TypeConversionUtils.image_X_UrlString(
track.album?.images,
placeholder: ImagePlaceholder.online,
),
),
duration: track.ytTrack.duration,
);
mobileAudioService?.addItem(tag);
_logger.v("[Track Direct Source] - ${(track).ytUri}");
this.track = track;
notifyListeners();
updatePersistence();
await player.play(
track.ytUri.startsWith("http")
? await getAppropriateSource(track, manifest)
: DeviceFileSource(track.ytUri),
);
status = PlaybackStatus.playing;
notifyListeners();
} }
Future<void> resume() async { Future<void> resume() async {
@ -382,7 +375,7 @@ class Playback extends PersistedChangeNotifier {
); );
return List.castFrom<dynamic, Map<String, int>>(segments); return List.castFrom<dynamic, Map<String, int>>(segments);
} catch (e, stack) { } catch (e, stack) {
_logger.e("[getSkipSegments]", e, stack); Catcher.reportCheckedError(e, stack);
return List.castFrom<dynamic, Map<String, int>>([]); return List.castFrom<dynamic, Map<String, int>>([]);
} }
} }
@ -465,112 +458,104 @@ class Playback extends PersistedChangeNotifier {
bool noSponsorBlock = false, bool noSponsorBlock = false,
bool ignoreCache = false, bool ignoreCache = false,
}) async { }) async {
try { final format = preferences.ytSearchFormat;
final format = preferences.ytSearchFormat; final matchAlgorithm = preferences.trackMatchAlgorithm;
final matchAlgorithm = preferences.trackMatchAlgorithm; final artistsName =
final artistsName = track.artists track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
?.map((ar) => ar.name) [];
.toList() _logger.v("[Track Search Artists] $artistsName");
.whereNotNull() final mainArtist = artistsName.first;
.toList() ?? final featuredArtists = artistsName.length > 1
[]; ? "feat. ${artistsName.sublist(1).join(" ")}"
_logger.v("[Track Search Artists] $artistsName"); : "";
final mainArtist = artistsName.first; final title = ServiceUtils.getTitle(
final featuredArtists = artistsName.length > 1 track.name!,
? "feat. ${artistsName.sublist(1).join(" ")}" artists: artistsName,
: ""; onlyCleanArtist: true,
final title = ServiceUtils.getTitle( ).trim();
track.name!, _logger.v("[Track Search Title] $title");
artists: artistsName, final queryString = format
onlyCleanArtist: true, .replaceAll("\$MAIN_ARTIST", mainArtist)
).trim(); .replaceAll("\$TITLE", title)
_logger.v("[Track Search Title] $title"); .replaceAll("\$FEATURED_ARTISTS", featuredArtists);
final queryString = format _logger.v("[Youtube Search Term] $queryString");
.replaceAll("\$MAIN_ARTIST", mainArtist)
.replaceAll("\$TITLE", title)
.replaceAll("\$FEATURED_ARTISTS", featuredArtists);
_logger.v("[Youtube Search Term] $queryString");
Video ytVideo; Video ytVideo;
final cachedTrack = await cache.get(track.id); final cachedTrack = await cache.get(track.id);
if (cachedTrack != null && if (cachedTrack != null &&
cachedTrack.mode == matchAlgorithm.name && cachedTrack.mode == matchAlgorithm.name &&
!ignoreCache) { !ignoreCache) {
_logger.v( _logger.v(
"[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}", "[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}",
);
ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
} else {
VideoSearchList videos =
await raceMultiple(() => youtube.search.search(queryString));
if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
List<Map> ratedRankedVideos = videos
.map((video) {
// the find should be lazy thus everything case insensitive
final ytTitle = video.title.toLowerCase();
final bool hasTitle = ytTitle.contains(title);
final bool hasAllArtists = track.artists?.every(
(artist) => ytTitle.contains(artist.name!.toLowerCase()),
) ??
false;
final bool authorIsArtist =
track.artists?.first.name?.toLowerCase() ==
video.author.toLowerCase();
final bool hasNoLiveInTitle =
!PrimitiveUtils.containsTextInBracket(ytTitle, "live");
final bool hasCloseDuration =
(track.duration!.inSeconds - video.duration!.inSeconds)
.abs() <=
10; //Duration matching threshold
int rate = 0;
for (final el in [
hasTitle,
hasAllArtists,
if (matchAlgorithm ==
SpotubeTrackMatchAlgorithm.authenticPopular)
authorIsArtist,
hasNoLiveInTitle,
hasCloseDuration,
!video.isLive,
]) {
if (el) rate++;
}
// can't let pass any non title matching track
if (!hasTitle) rate = rate - 2;
return {
"video": video,
"points": rate,
"views": video.engagement.viewCount,
};
})
.toList()
.sortByProperties(
[false, false],
["points", "views"],
);
ytVideo = ratedRankedVideos.first["video"] as Video;
_siblingYtVideos =
ratedRankedVideos.map((e) => e["video"] as Video).toList();
notifyListeners();
} else {
ytVideo = videos.where((video) => !video.isLive).first;
_siblingYtVideos = videos.take(10).toList();
notifyListeners();
}
}
return ytVideoToSpotubeTrack(
ytVideo,
track,
noSponsorBlock: noSponsorBlock,
); );
} catch (e, stack) { ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
_logger.e("topSpotubeTrack", e, stack); } else {
rethrow; VideoSearchList videos =
await raceMultiple(() => youtube.search.search(queryString));
if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
List<Map> ratedRankedVideos = videos
.map((video) {
// the find should be lazy thus everything case insensitive
final ytTitle = video.title.toLowerCase();
final bool hasTitle = ytTitle.contains(title);
final bool hasAllArtists = track.artists?.every(
(artist) => ytTitle.contains(artist.name!.toLowerCase()),
) ??
false;
final bool authorIsArtist =
track.artists?.first.name?.toLowerCase() ==
video.author.toLowerCase();
final bool hasNoLiveInTitle =
!PrimitiveUtils.containsTextInBracket(ytTitle, "live");
final bool hasCloseDuration =
(track.duration!.inSeconds - video.duration!.inSeconds)
.abs() <=
10; //Duration matching threshold
int rate = 0;
for (final el in [
hasTitle,
hasAllArtists,
if (matchAlgorithm ==
SpotubeTrackMatchAlgorithm.authenticPopular)
authorIsArtist,
hasNoLiveInTitle,
hasCloseDuration,
!video.isLive,
]) {
if (el) rate++;
}
// can't let pass any non title matching track
if (!hasTitle) rate = rate - 2;
return {
"video": video,
"points": rate,
"views": video.engagement.viewCount,
};
})
.toList()
.sortByProperties(
[false, false],
["points", "views"],
);
ytVideo = ratedRankedVideos.first["video"] as Video;
_siblingYtVideos =
ratedRankedVideos.map((e) => e["video"] as Video).toList();
notifyListeners();
} else {
ytVideo = videos.where((video) => !video.isLive).first;
_siblingYtVideos = videos.take(10).toList();
notifyListeners();
}
} }
return ytVideoToSpotubeTrack(
ytVideo,
track,
noSponsorBlock: noSponsorBlock,
);
} }
Future<Source> getAppropriateSource( Future<Source> getAppropriateSource(
@ -650,24 +635,20 @@ class Playback extends PersistedChangeNotifier {
@override @override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) async { FutureOr<void> loadFromLocal(Map<String, dynamic> map) async {
try { if (map["playlist"] != null) {
if (map["playlist"] != null) { playlist = CurrentPlaylist.fromJson(jsonDecode(map["playlist"]));
playlist = CurrentPlaylist.fromJson(jsonDecode(map["playlist"]));
}
if (map["track"] != null) {
final Map<String, dynamic> trackMap = jsonDecode(map["track"]);
// for backwards compatibility
if (!trackMap.containsKey("skipSegments")) {
trackMap["skipSegments"] = await getSkipSegments(
trackMap["id"],
);
}
track = SpotubeTrack.fromJson(trackMap);
}
volume = map["volume"] ?? volume;
} catch (e, stack) {
_logger.e("loadFromLocal", e, stack);
} }
if (map["track"] != null) {
final Map<String, dynamic> trackMap = jsonDecode(map["track"]);
// for backwards compatibility
if (!trackMap.containsKey("skipSegments")) {
trackMap["skipSegments"] = await getSkipSegments(
trackMap["id"],
);
}
track = SpotubeTrack.fromJson(trackMap);
}
volume = map["volume"] ?? volume;
} }
@override @override

View File

@ -283,44 +283,39 @@ class _MprisMediaPlayer2Player extends DBusObject {
/// Gets value of property org.mpris.MediaPlayer2.Player.Metadata /// Gets value of property org.mpris.MediaPlayer2.Player.Metadata
Future<DBusMethodResponse> getMetadata() async { Future<DBusMethodResponse> getMetadata() async {
try { if (playback.track == null) {
if (playback.track == null) { return DBusMethodSuccessResponse([DBusDict.stringVariant({})]);
return DBusMethodSuccessResponse([DBusDict.stringVariant({})]);
}
final id = (playback.playlist != null
? playback.playlist!.tracks.indexWhere(
(track) => playback.track!.id == track.id!,
)
: 0)
.abs();
return DBusMethodSuccessResponse([
DBusDict.stringVariant({
"mpris:trackid": DBusString("${path.value}/Track/$id"),
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
"mpris:artUrl": DBusString(
TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
),
"xesam:album": DBusString(playback.track!.album!.name!),
"xesam:artist": DBusArray.string(
playback.track!.artists!.map((artist) => artist.name!),
),
"xesam:title": DBusString(playback.track!.name!),
"xesam:url": DBusString(
playback.track is SpotubeTrack
? (playback.track as SpotubeTrack).ytUri
: playback.track!.previewUrl!,
),
"xesam:genre": const DBusString("Unknown"),
}),
]);
} catch (e) {
print("[DBUS ERROR] $e");
rethrow;
} }
final id = (playback.playlist != null
? playback.playlist!.tracks.indexWhere(
(track) => playback.track!.id == track.id!,
)
: 0)
.abs();
return DBusMethodSuccessResponse([
DBusDict.stringVariant({
"mpris:trackid": DBusString("${path.value}/Track/$id"),
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
"mpris:artUrl": DBusString(
TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
),
"xesam:album": DBusString(playback.track!.album!.name!),
"xesam:artist": DBusArray.string(
playback.track!.artists!.map((artist) => artist.name!),
),
"xesam:title": DBusString(playback.track!.name!),
"xesam:url": DBusString(
playback.track is SpotubeTrack
? (playback.track as SpotubeTrack).ytUri
: playback.track!.previewUrl!,
),
"xesam:genre": const DBusString("Unknown"),
}),
]);
} }
/// Gets value of property org.mpris.MediaPlayer2.Player.Volume /// Gets value of property org.mpris.MediaPlayer2.Player.Volume

View File

@ -1,3 +1,5 @@
import 'package:catcher/catcher.dart';
/// Parses duration string formatted by Duration.toString() to [Duration]. /// Parses duration string formatted by Duration.toString() to [Duration].
/// The string should be of form hours:minutes:seconds.microseconds /// The string should be of form hours:minutes:seconds.microseconds
/// ///
@ -50,7 +52,8 @@ Duration parseDuration(String input) {
Duration? tryParseDuration(String input) { Duration? tryParseDuration(String input) {
try { try {
return parseDuration(input); return parseDuration(input);
} catch (_) { } catch (e, stack) {
Catcher.reportCheckedError(e, stack);
return null; return null;
} }
} }

View File

@ -54,33 +54,28 @@ abstract class ServiceUtils {
} }
static Future<String?> extractLyrics(Uri url) async { static Future<String?> extractLyrics(Uri url) async {
try { final response = await http.get(url);
var response = await http.get(url);
Document document = parser.parse(response.body); Document document = parser.parse(response.body);
var lyrics = document.querySelector('div.lyrics')?.text.trim(); String? lyrics = document.querySelector('div.lyrics')?.text.trim();
if (lyrics == null) { if (lyrics == null) {
lyrics = ""; lyrics = "";
document document
.querySelectorAll("div[class^=\"Lyrics__Container\"]") .querySelectorAll("div[class^=\"Lyrics__Container\"]")
.forEach((element) { .forEach((element) {
if (element.text.trim().isNotEmpty) { if (element.text.trim().isNotEmpty) {
var snippet = element.innerHtml.replaceAll("<br>", "\n").replaceAll( final snippet = element.innerHtml.replaceAll("<br>", "\n").replaceAll(
RegExp("<(?!\\s*br\\s*\\/?)[^>]+>", caseSensitive: false), RegExp("<(?!\\s*br\\s*\\/?)[^>]+>", caseSensitive: false),
"", "",
); );
var el = document.createElement("textarea"); final el = document.createElement("textarea");
el.innerHtml = snippet; el.innerHtml = snippet;
lyrics = "$lyrics${el.text.trim()}\n\n"; lyrics = "$lyrics${el.text.trim()}\n\n";
} }
}); });
}
return lyrics;
} catch (e, stack) {
logger.e("extractLyrics", e, stack);
rethrow;
} }
return lyrics;
} }
static Future<List?> searchSong( static Future<List?> searchSong(
@ -90,36 +85,31 @@ abstract class ServiceUtils {
bool optimizeQuery = false, bool optimizeQuery = false,
bool authHeader = false, bool authHeader = false,
}) async { }) async {
try { if (apiKey == "" || apiKey == null) {
if (apiKey == "" || apiKey == null) { apiKey = PrimitiveUtils.getRandomElement(lyricsSecrets);
apiKey = PrimitiveUtils.getRandomElement(lyricsSecrets);
}
const searchUrl = 'https://api.genius.com/search?q=';
String song =
optimizeQuery ? getTitle(title, artists: artist) : "$title $artist";
String reqUrl = "$searchUrl${Uri.encodeComponent(song)}";
Map<String, String> headers = {"Authorization": 'Bearer $apiKey'};
final response = await http.get(
Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"),
headers: authHeader ? headers : null,
);
Map data = jsonDecode(response.body)["response"];
if (data["hits"]?.length == 0) return null;
List results = data["hits"]?.map((val) {
return <String, dynamic>{
"id": val["result"]["id"],
"full_title": val["result"]["full_title"],
"albumArt": val["result"]["song_art_image_url"],
"url": val["result"]["url"],
"author": val["result"]["primary_artist"]["name"],
};
}).toList();
return results;
} catch (e, stack) {
logger.e("searchSong", e, stack);
rethrow;
} }
const searchUrl = 'https://api.genius.com/search?q=';
String song =
optimizeQuery ? getTitle(title, artists: artist) : "$title $artist";
String reqUrl = "$searchUrl${Uri.encodeComponent(song)}";
Map<String, String> headers = {"Authorization": 'Bearer $apiKey'};
final response = await http.get(
Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"),
headers: authHeader ? headers : null,
);
Map data = jsonDecode(response.body)["response"];
if (data["hits"]?.length == 0) return null;
List results = data["hits"]?.map((val) {
return <String, dynamic>{
"id": val["result"]["id"],
"full_title": val["result"]["full_title"],
"albumArt": val["result"]["song_art_image_url"],
"url": val["result"]["url"],
"author": val["result"]["primary_artist"]["name"],
};
}).toList();
return results;
} }
static Future<String?> getLyrics( static Future<String?> getLyrics(
@ -129,49 +119,44 @@ abstract class ServiceUtils {
bool optimizeQuery = false, bool optimizeQuery = false,
bool authHeader = false, bool authHeader = false,
}) async { }) async {
try { final results = await searchSong(
final results = await searchSong( title,
title, artists,
artists, apiKey: apiKey,
apiKey: apiKey, optimizeQuery: optimizeQuery,
optimizeQuery: optimizeQuery, authHeader: authHeader,
authHeader: authHeader, );
); if (results == null) return null;
if (results == null) return null; title = getTitle(
title = getTitle( title,
title, artists: artists,
artists: artists, onlyCleanArtist: true,
onlyCleanArtist: true, ).trim();
).trim(); final ratedLyrics = results.map((result) {
final ratedLyrics = results.map((result) { final gTitle = (result["full_title"] as String).toLowerCase();
final gTitle = (result["full_title"] as String).toLowerCase(); int points = 0;
int points = 0; final hasTitle = gTitle.contains(title);
final hasTitle = gTitle.contains(title); final hasAllArtists =
final hasAllArtists = artists.every((artist) => gTitle.contains(artist.toLowerCase()));
artists.every((artist) => gTitle.contains(artist.toLowerCase())); final String lyricAuthor = result["author"].toLowerCase();
final String lyricAuthor = result["author"].toLowerCase(); final fromOriginalAuthor =
final fromOriginalAuthor = lyricAuthor.contains(artists.first.toLowerCase());
lyricAuthor.contains(artists.first.toLowerCase());
for (final criteria in [ for (final criteria in [
hasTitle, hasTitle,
hasAllArtists, hasAllArtists,
fromOriginalAuthor, fromOriginalAuthor,
]) { ]) {
if (criteria) points++; if (criteria) points++;
} }
return {"result": result, "points": points}; return {"result": result, "points": points};
}).sorted( }).sorted(
(a, b) => ((a["points"] as int).compareTo(a["points"] as int)), (a, b) => ((a["points"] as int).compareTo(a["points"] as int)),
); );
final worthyOne = ratedLyrics.first["result"]; final worthyOne = ratedLyrics.first["result"];
String? lyrics = await extractLyrics(Uri.parse(worthyOne["url"])); String? lyrics = await extractLyrics(Uri.parse(worthyOne["url"]));
return lyrics; return lyrics;
} catch (e, stack) {
logger.e("getLyrics", e, stack);
return null;
}
} }
static const baseUri = "https://www.rentanadviser.com/subtitles"; static const baseUri = "https://www.rentanadviser.com/subtitles";
@ -263,24 +248,19 @@ abstract class ServiceUtils {
static Future<SpotifySpotubeCredentials> getAccessToken( static Future<SpotifySpotubeCredentials> getAccessToken(
String cookieHeader) async { String cookieHeader) async {
try { final res = await http.get(
final res = await http.get( Uri.parse(
Uri.parse( "https://open.spotify.com/get_access_token?reason=transport&productType=web_player",
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player", ),
), headers: {
headers: { "Cookie": cookieHeader,
"Cookie": cookieHeader, "User-Agent":
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" },
}, );
); return SpotifySpotubeCredentials.fromJson(
return SpotifySpotubeCredentials.fromJson( jsonDecode(res.body),
jsonDecode(res.body), );
);
} catch (e, stack) {
logger.e("getAccessToken", e, stack);
rethrow;
}
} }
static void navigate(BuildContext context, String location, {Object? extra}) { static void navigate(BuildContext context, String location, {Object? extra}) {

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h> #include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <catcher/catcher_plugin.h>
#include <metadata_god/metadata_god_plugin.h> #include <metadata_god/metadata_god_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) catcher_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "CatcherPlugin");
catcher_plugin_register_with_registrar(catcher_registrar);
g_autoptr(FlPluginRegistrar) metadata_god_registrar = g_autoptr(FlPluginRegistrar) metadata_god_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MetadataGodPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "MetadataGodPlugin");
metadata_god_plugin_register_with_registrar(metadata_god_registrar); metadata_god_plugin_register_with_registrar(metadata_god_registrar);

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux audioplayers_linux
catcher
metadata_god metadata_god
screen_retriever screen_retriever
url_launcher_linux url_launcher_linux

View File

@ -8,10 +8,12 @@ import Foundation
import audio_service import audio_service
import audio_session import audio_session
import audioplayers_darwin import audioplayers_darwin
import catcher
import connectivity_plus_macos import connectivity_plus_macos
import device_info_plus
import macos_ui import macos_ui
import metadata_god import metadata_god
import package_info_plus_macos import package_info_plus
import path_provider_foundation import path_provider_foundation
import screen_retriever import screen_retriever
import shared_preferences_foundation import shared_preferences_foundation
@ -24,7 +26,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin"))
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin"))
MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin")) MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))

View File

@ -281,6 +281,15 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
catcher:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "5ccf7c0d8bbb07329667cff561aede1a9bee4934"
url: "https://github.com/ThexXTURBOXx/catcher"
source: git
version: "0.7.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -428,6 +437,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.8" version: "0.7.8"
device_info_plus:
dependency: transitive
description:
name: device_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "7.0.0"
dio:
dependency: transitive
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.6"
dots_indicator: dots_indicator:
dependency: transitive dependency: transitive
description: description:
@ -599,6 +629,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_mailer:
dependency: transitive
description:
name: flutter_mailer
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -637,6 +674,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast:
dependency: transitive
description:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.2"
freezed_annotation: freezed_annotation:
dependency: transitive dependency: transitive
description: description:
@ -826,6 +870,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.6" version: "1.7.6"
mailer:
dependency: transitive
description:
name: mailer
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
marquee: marquee:
dependency: "direct main" dependency: "direct main"
description: description:
@ -902,42 +953,14 @@ packages:
name: package_info_plus name: package_info_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.3+1" version: "3.0.2"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "2.0.1"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.6"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
palette_generator: palette_generator:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1185,6 +1208,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
sentry:
dependency: transitive
description:
name: sentry
url: "https://pub.dartlang.org"
source: hosted
version: "6.19.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1393,6 +1423,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
universal_io:
dependency: transitive
description:
name: universal_io
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -18,6 +18,9 @@ dependencies:
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
badges: ^2.0.3 badges: ^2.0.3
cached_network_image: ^3.2.2 cached_network_image: ^3.2.2
catcher:
git:
url: https://github.com/ThexXTURBOXx/catcher
collection: ^1.15.0 collection: ^1.15.0
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
dbus: ^0.7.8 dbus: ^0.7.8
@ -47,7 +50,7 @@ dependencies:
marquee: ^2.2.3 marquee: ^2.2.3
metadata_god: ^0.3.2 metadata_god: ^0.3.2
mime: ^1.0.2 mime: ^1.0.2
package_info_plus: ^1.4.3 package_info_plus: ^3.0.2
palette_generator: ^0.3.3 palette_generator: ^0.3.3
path: ^1.8.0 path: ^1.8.0
path_provider: ^2.0.8 path_provider: ^2.0.8
@ -85,6 +88,9 @@ dev_dependencies:
sdk: flutter sdk: flutter
hive_generator: ^2.0.0 hive_generator: ^2.0.0
dependency_overrides:
package_info_plus: ^3.0.2
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h> #include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <catcher/catcher_plugin.h>
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h> #include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
#include <metadata_god/metadata_god_plugin_c_api.h> #include <metadata_god/metadata_god_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
@ -18,6 +19,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar( AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
CatcherPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CatcherPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
MetadataGodPluginCApiRegisterWithRegistrar( MetadataGodPluginCApiRegisterWithRegistrar(

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows audioplayers_windows
catcher
connectivity_plus_windows connectivity_plus_windows
metadata_god metadata_god
permission_handler_windows permission_handler_windows