From 84d94b05bc269a1676a261df2b12e508e10e4c0e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 1 Feb 2023 18:26:17 +0600 Subject: [PATCH] feat: use catcher to handle exceptions --- lib/collections/intents.dart | 47 ++- lib/collections/routes.dart | 3 +- lib/collections/spotube_icons.dart | 2 +- lib/components/library/user_local_tracks.dart | 10 +- lib/components/player/player_controls.dart | 10 +- lib/components/root/bottom_player.dart | 12 +- lib/generated_plugin_registrant.dart | 2 - lib/hooks/playback_hooks.dart | 27 +- lib/main.dart | 165 +++++---- lib/models/logger.dart | 16 + lib/pages/artist/artist.dart | 6 - lib/provider/downloader_provider.dart | 8 +- lib/provider/playback_provider.dart | 347 +++++++++--------- lib/services/linux_audio_service.dart | 69 ++-- lib/utils/duration.dart | 5 +- lib/utils/service_utils.dart | 206 +++++------ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 +- pubspec.lock | 97 +++-- pubspec.yaml | 8 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 23 files changed, 532 insertions(+), 523 deletions(-) diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index e77ef5c9..42e5ace7 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -20,33 +20,28 @@ class PlayPauseAction extends Action { @override invoke(intent) async { - try { - if (PlayerControls.focusNode.canRequestFocus) { - 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; + if (PlayerControls.focusNode.canRequestFocus) { + 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; } } diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 47b28403..aa04d5dc 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -1,3 +1,4 @@ +import 'package:catcher/catcher.dart'; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; 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/mobile_login/mobile_login.dart'; -final rootNavigatorKey = GlobalKey(); +final rootNavigatorKey = Catcher.navigatorKey; final shellRouteNavigatorKey = GlobalKey(); final router = GoRouter( navigatorKey: rootNavigatorKey, diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 7fe227ee..4b72aa6a 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -60,5 +60,5 @@ abstract class SpotubeIcons { static const info = FeatherIcons.info; static const userRemove = FeatherIcons.userX; static const close = FeatherIcons.x; - static const minimize = FeatherIcons.minimize2; + static const minimize = FeatherIcons.chevronDown; } diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index a855502d..499c49c1 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:catcher/catcher.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -98,12 +99,11 @@ final localTracksProvider = FutureProvider>((ref) async { } on FfiException catch (e) { if (e.message == "NoTag: reader does not contain an id3 tag") { getLogger(FutureProvider>) - .w("[Fetching metadata]", e.message); + .v("[Fetching metadata]", e.message); } return {}; - } on Exception catch (e, stack) { - getLogger(FutureProvider>) - .e("[Fetching metadata]", e, stack); + } catch (e, stack) { + Catcher.reportCheckedError(e, stack); return {}; } }, @@ -124,7 +124,7 @@ final localTracksProvider = FutureProvider>((ref) async { return tracks; } catch (e, stack) { - getLogger(FutureProvider).e("[LocalTracksProvider]", e, stack); + Catcher.reportCheckedError(e, stack); return []; } }); diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index 9ee6df1b..57b6f5d3 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -199,15 +199,7 @@ class PlayerControls extends HookConsumerWidget { SpotubeIcons.stop, color: iconColor, ), - onPressed: playback.track != null - ? () async { - try { - await playback.stop(); - } catch (e, stack) { - logger.e("onStop", e, stack); - } - } - : null, + onPressed: playback.track != null ? playback.stop : null, ), PlatformIconButton( tooltip: diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 5eb59661..83c2b7c2 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -145,14 +145,10 @@ class BottomPlayer extends HookConsumerWidget { volume.value = v; }, onChangeEnd: (value) async { - try { - // You don't really need to know why but this - // way it works only - await playback.setVolume(value); - await playback.setVolume(value); - } catch (e, stack) { - logger.e("onChange", e, stack); - } + // You don't really need to know why but this + // way it works only + await playback.setVolume(value); + await playback.setVolume(value); }, ), ); diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart index 22458492..938e48db 100644 --- a/lib/generated_plugin_registrant.dart +++ b/lib/generated_plugin_registrant.dart @@ -10,7 +10,6 @@ import 'package:audio_service_web/audio_service_web.dart'; import 'package:audio_session/audio_session_web.dart'; import 'package:audioplayers_web/audioplayers_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:url_launcher_web/url_launcher_web.dart'; @@ -22,7 +21,6 @@ void registerPlugins(Registrar registrar) { AudioSessionWeb.registerWith(registrar); AudioplayersPlugin.registerWith(registrar); FilePickerWeb.registerWith(registrar); - PackageInfoPlugin.registerWith(registrar); SharedPreferencesPlugin.registerWith(registrar); UrlLauncherPlugin.registerWith(registrar); registrar.registerMessageHandler(); diff --git a/lib/hooks/playback_hooks.dart b/lib/hooks/playback_hooks.dart index 266ac9b8..c5dfcec9 100644 --- a/lib/hooks/playback_hooks.dart +++ b/lib/hooks/playback_hooks.dart @@ -1,31 +1,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/playback_provider.dart'; -final logger = getLogger("PlaybackHook"); - Future Function() useNextTrack(WidgetRef ref) { return () async { - try { - final playback = ref.read(playbackProvider); - await playback.player.pause(); - await playback.player.seek(Duration.zero); - playback.seekForward(); - } catch (e, stack) { - logger.e("useNextTrack", e, stack); - } + final playback = ref.read(playbackProvider); + await playback.player.pause(); + await playback.player.seek(Duration.zero); + playback.seekForward(); }; } Future Function() usePreviousTrack(WidgetRef ref) { return () async { - try { - final playback = ref.read(playbackProvider); - await playback.player.pause(); - await playback.player.seek(Duration.zero); - playback.seekBackward(); - } catch (e, stack) { - logger.e("onPrevious", e, stack); - } + final playback = ref.read(playbackProvider); + await playback.player.pause(); + await playback.player.seek(Duration.zero); + playback.seekBackward(); }; } diff --git a/lib/main.dart b/lib/main.dart index 608aed0a..6f7730d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:audio_service/audio_service.dart'; +import 'package:catcher/catcher.dart'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -61,82 +62,98 @@ void main() async { }); } 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( - 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( - 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(), - ), - ); - }, + Catcher( + debugConfig: CatcherOptions( + SilentReportMode(), + [ + ConsoleHandler( + enableDeviceParameters: false, + enableApplicationParameters: false, + ), + FileHandler(await getLogsPath(), printLogs: false), + SnackbarHandler(const Duration(seconds: 5)), + ], ), + 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( + 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(), + ), + ); + }, + ), + ); + }, ); } diff --git a/lib/models/logger.dart b/lib/models/logger.dart index ab20e080..9237c818 100644 --- a/lib/models/logger.dart +++ b/lib/models/logger.dart @@ -13,6 +13,22 @@ SpotubeLogger getLogger(T owner) { return _loggerFactory; } +Future 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 { String? owner; SpotubeLogger([this.owner]) : super(filter: _SpotubeLogFilter()); diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 9287e594..bc1b6b72 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -203,12 +203,6 @@ class ArtistPage extends HookConsumerWidget { .queryKey, ) ?.refetch(); - } catch (e, stack) { - logger.e( - "FollowButton.onPressed", - e, - stack, - ); } finally { QueryBowl.of(context) .refetchQueries([ diff --git a/lib/provider/downloader_provider.dart b/lib/provider/downloader_provider.dart index 26ceda0f..21693a83 100644 --- a/lib/provider/downloader_provider.dart +++ b/lib/provider/downloader_provider.dart @@ -134,12 +134,8 @@ class Downloader with ChangeNotifier { logger.v( "[addToQueue] Writing metadata to ${file.path} is successful", ); - } catch (e, stack) { - logger.e( - "[addToQueue] Failed download of ${file.path}", - e, - stack, - ); + } catch (e) { + logger.v("[addToQueue] Failed download of ${file.path}", e); rethrow; } finally { currentlyRunning--; diff --git a/lib/provider/playback_provider.dart b/lib/provider/playback_provider.dart index e78bc06e..767a11f7 100644 --- a/lib/provider/playback_provider.dart +++ b/lib/provider/playback_provider.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:audio_service/audio_service.dart'; import 'package:audioplayers/audioplayers.dart'; +import 'package:catcher/catcher.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive/hive.dart'; @@ -198,75 +199,67 @@ class Playback extends PersistedChangeNotifier { } Future playPlaylist(CurrentPlaylist playlist, [int index = 0]) async { - try { - if (index < 0 || index > playlist.tracks.length - 1) return; - if (isPlaying || status == PlaybackStatus.playing) await stop(); - this.playlist = blacklistNotifier.filterPlaylist(playlist); - mobileAudioService?.session?.setActive(true); - final played = this.playlist!.tracks[index]; - status = PlaybackStatus.loading; - notifyListeners(); - await play(played).then((_) { - int i = this - .playlist! - .tracks - .indexWhere((element) => element.id == played.id); - if (index == -1) return; - this.playlist!.tracks[i] = track!; - }); - } catch (e) { - _logger.e("[playPlaylist] $e"); - } + if (index < 0 || index > playlist.tracks.length - 1) return; + if (isPlaying || status == PlaybackStatus.playing) await stop(); + this.playlist = blacklistNotifier.filterPlaylist(playlist); + mobileAudioService?.session?.setActive(true); + final played = this.playlist!.tracks[index]; + status = PlaybackStatus.loading; + notifyListeners(); + await play(played).then((_) { + int i = this + .playlist! + .tracks + .indexWhere((element) => element.id == played.id); + if (index == -1) return; + this.playlist!.tracks[i] = track!; + }); } // player methods Future play(Track track, {AudioOnlyStreamInfo? manifest}) async { _logger.v("[Track Playing] ${track.name} - ${track.id}"); - try { - // the track is already playing so no need to change that - if (track.id == this.track?.id) return; - if (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 ?? []), - 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; + // the track is already playing so no need to change that + if (track.id == this.track?.id) return; + if (status != PlaybackStatus.loading) { + status = PlaybackStatus.loading; 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 ?? []), + 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 resume() async { @@ -382,7 +375,7 @@ class Playback extends PersistedChangeNotifier { ); return List.castFrom>(segments); } catch (e, stack) { - _logger.e("[getSkipSegments]", e, stack); + Catcher.reportCheckedError(e, stack); return List.castFrom>([]); } } @@ -465,112 +458,104 @@ class Playback extends PersistedChangeNotifier { bool noSponsorBlock = false, bool ignoreCache = false, }) async { - try { - final format = preferences.ytSearchFormat; - final matchAlgorithm = preferences.trackMatchAlgorithm; - final artistsName = track.artists - ?.map((ar) => ar.name) - .toList() - .whereNotNull() - .toList() ?? - []; - _logger.v("[Track Search Artists] $artistsName"); - final mainArtist = artistsName.first; - final featuredArtists = artistsName.length > 1 - ? "feat. ${artistsName.sublist(1).join(" ")}" - : ""; - final title = ServiceUtils.getTitle( - track.name!, - artists: artistsName, - onlyCleanArtist: true, - ).trim(); - _logger.v("[Track Search Title] $title"); - final queryString = format - .replaceAll("\$MAIN_ARTIST", mainArtist) - .replaceAll("\$TITLE", title) - .replaceAll("\$FEATURED_ARTISTS", featuredArtists); - _logger.v("[Youtube Search Term] $queryString"); + final format = preferences.ytSearchFormat; + final matchAlgorithm = preferences.trackMatchAlgorithm; + final artistsName = + track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? + []; + _logger.v("[Track Search Artists] $artistsName"); + final mainArtist = artistsName.first; + final featuredArtists = artistsName.length > 1 + ? "feat. ${artistsName.sublist(1).join(" ")}" + : ""; + final title = ServiceUtils.getTitle( + track.name!, + artists: artistsName, + onlyCleanArtist: true, + ).trim(); + _logger.v("[Track Search Title] $title"); + final queryString = format + .replaceAll("\$MAIN_ARTIST", mainArtist) + .replaceAll("\$TITLE", title) + .replaceAll("\$FEATURED_ARTISTS", featuredArtists); + _logger.v("[Youtube Search Term] $queryString"); - Video ytVideo; - final cachedTrack = await cache.get(track.id); - if (cachedTrack != null && - cachedTrack.mode == matchAlgorithm.name && - !ignoreCache) { - _logger.v( - "[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 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, + Video ytVideo; + final cachedTrack = await cache.get(track.id); + if (cachedTrack != null && + cachedTrack.mode == matchAlgorithm.name && + !ignoreCache) { + _logger.v( + "[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}", ); - } catch (e, stack) { - _logger.e("topSpotubeTrack", e, stack); - rethrow; + ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack); + } else { + VideoSearchList videos = + await raceMultiple(() => youtube.search.search(queryString)); + if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) { + List 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 getAppropriateSource( @@ -650,24 +635,20 @@ class Playback extends PersistedChangeNotifier { @override FutureOr loadFromLocal(Map map) async { - try { - if (map["playlist"] != null) { - playlist = CurrentPlaylist.fromJson(jsonDecode(map["playlist"])); - } - if (map["track"] != null) { - final Map 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["playlist"] != null) { + playlist = CurrentPlaylist.fromJson(jsonDecode(map["playlist"])); } + if (map["track"] != null) { + final Map 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 diff --git a/lib/services/linux_audio_service.dart b/lib/services/linux_audio_service.dart index 8cb82306..9b15ad0f 100644 --- a/lib/services/linux_audio_service.dart +++ b/lib/services/linux_audio_service.dart @@ -283,44 +283,39 @@ class _MprisMediaPlayer2Player extends DBusObject { /// Gets value of property org.mpris.MediaPlayer2.Player.Metadata Future getMetadata() async { - try { - if (playback.track == null) { - 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; + if (playback.track == null) { + 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"), + }), + ]); } /// Gets value of property org.mpris.MediaPlayer2.Player.Volume diff --git a/lib/utils/duration.dart b/lib/utils/duration.dart index 7d3cd58e..858503fb 100644 --- a/lib/utils/duration.dart +++ b/lib/utils/duration.dart @@ -1,3 +1,5 @@ +import 'package:catcher/catcher.dart'; + /// Parses duration string formatted by Duration.toString() to [Duration]. /// The string should be of form hours:minutes:seconds.microseconds /// @@ -50,7 +52,8 @@ Duration parseDuration(String input) { Duration? tryParseDuration(String input) { try { return parseDuration(input); - } catch (_) { + } catch (e, stack) { + Catcher.reportCheckedError(e, stack); return null; } } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index f4b7dd76..3bdef94d 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -54,33 +54,28 @@ abstract class ServiceUtils { } static Future extractLyrics(Uri url) async { - try { - var response = await http.get(url); + final response = await http.get(url); - Document document = parser.parse(response.body); - var lyrics = document.querySelector('div.lyrics')?.text.trim(); - if (lyrics == null) { - lyrics = ""; - document - .querySelectorAll("div[class^=\"Lyrics__Container\"]") - .forEach((element) { - if (element.text.trim().isNotEmpty) { - var snippet = element.innerHtml.replaceAll("
", "\n").replaceAll( - RegExp("<(?!\\s*br\\s*\\/?)[^>]+>", caseSensitive: false), - "", - ); - var el = document.createElement("textarea"); - el.innerHtml = snippet; - lyrics = "$lyrics${el.text.trim()}\n\n"; - } - }); - } - - return lyrics; - } catch (e, stack) { - logger.e("extractLyrics", e, stack); - rethrow; + Document document = parser.parse(response.body); + String? lyrics = document.querySelector('div.lyrics')?.text.trim(); + if (lyrics == null) { + lyrics = ""; + document + .querySelectorAll("div[class^=\"Lyrics__Container\"]") + .forEach((element) { + if (element.text.trim().isNotEmpty) { + final snippet = element.innerHtml.replaceAll("
", "\n").replaceAll( + RegExp("<(?!\\s*br\\s*\\/?)[^>]+>", caseSensitive: false), + "", + ); + final el = document.createElement("textarea"); + el.innerHtml = snippet; + lyrics = "$lyrics${el.text.trim()}\n\n"; + } + }); } + + return lyrics; } static Future searchSong( @@ -90,36 +85,31 @@ abstract class ServiceUtils { bool optimizeQuery = false, bool authHeader = false, }) async { - try { - if (apiKey == "" || apiKey == null) { - 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 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 { - "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; + if (apiKey == "" || apiKey == null) { + 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 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 { + "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 getLyrics( @@ -129,49 +119,44 @@ abstract class ServiceUtils { bool optimizeQuery = false, bool authHeader = false, }) async { - try { - final results = await searchSong( - title, - artists, - apiKey: apiKey, - optimizeQuery: optimizeQuery, - authHeader: authHeader, - ); - if (results == null) return null; - title = getTitle( - title, - artists: artists, - onlyCleanArtist: true, - ).trim(); - final ratedLyrics = results.map((result) { - final gTitle = (result["full_title"] as String).toLowerCase(); - int points = 0; - final hasTitle = gTitle.contains(title); - final hasAllArtists = - artists.every((artist) => gTitle.contains(artist.toLowerCase())); - final String lyricAuthor = result["author"].toLowerCase(); - final fromOriginalAuthor = - lyricAuthor.contains(artists.first.toLowerCase()); + final results = await searchSong( + title, + artists, + apiKey: apiKey, + optimizeQuery: optimizeQuery, + authHeader: authHeader, + ); + if (results == null) return null; + title = getTitle( + title, + artists: artists, + onlyCleanArtist: true, + ).trim(); + final ratedLyrics = results.map((result) { + final gTitle = (result["full_title"] as String).toLowerCase(); + int points = 0; + final hasTitle = gTitle.contains(title); + final hasAllArtists = + artists.every((artist) => gTitle.contains(artist.toLowerCase())); + final String lyricAuthor = result["author"].toLowerCase(); + final fromOriginalAuthor = + lyricAuthor.contains(artists.first.toLowerCase()); - for (final criteria in [ - hasTitle, - hasAllArtists, - fromOriginalAuthor, - ]) { - if (criteria) points++; - } - return {"result": result, "points": points}; - }).sorted( - (a, b) => ((a["points"] as int).compareTo(a["points"] as int)), - ); - final worthyOne = ratedLyrics.first["result"]; + for (final criteria in [ + hasTitle, + hasAllArtists, + fromOriginalAuthor, + ]) { + if (criteria) points++; + } + return {"result": result, "points": points}; + }).sorted( + (a, b) => ((a["points"] as int).compareTo(a["points"] as int)), + ); + final worthyOne = ratedLyrics.first["result"]; - String? lyrics = await extractLyrics(Uri.parse(worthyOne["url"])); - return lyrics; - } catch (e, stack) { - logger.e("getLyrics", e, stack); - return null; - } + String? lyrics = await extractLyrics(Uri.parse(worthyOne["url"])); + return lyrics; } static const baseUri = "https://www.rentanadviser.com/subtitles"; @@ -263,24 +248,19 @@ abstract class ServiceUtils { static Future getAccessToken( String cookieHeader) async { - try { - final res = await http.get( - Uri.parse( - "https://open.spotify.com/get_access_token?reason=transport&productType=web_player", - ), - headers: { - "Cookie": cookieHeader, - "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" - }, - ); - return SpotifySpotubeCredentials.fromJson( - jsonDecode(res.body), - ); - } catch (e, stack) { - logger.e("getAccessToken", e, stack); - rethrow; - } + final res = await http.get( + Uri.parse( + "https://open.spotify.com/get_access_token?reason=transport&productType=web_player", + ), + headers: { + "Cookie": cookieHeader, + "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" + }, + ); + return SpotifySpotubeCredentials.fromJson( + jsonDecode(res.body), + ); } static void navigate(BuildContext context, String location, {Object? extra}) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 73dfb386..49caffbf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); 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 = fl_plugin_registry_get_registrar_for_plugin(registry, "MetadataGodPlugin"); metadata_god_plugin_register_with_registrar(metadata_god_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 60021f16..81a1f2e1 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux + catcher metadata_god screen_retriever url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 76b6998e..f8f3c3c0 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,10 +8,12 @@ import Foundation import audio_service import audio_session import audioplayers_darwin +import catcher import connectivity_plus_macos +import device_info_plus import macos_ui import metadata_god -import package_info_plus_macos +import package_info_plus import path_provider_foundation import screen_retriever import shared_preferences_foundation @@ -24,7 +26,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ecbb633b..ba3f2d4c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -281,6 +281,15 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -428,6 +437,27 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -599,6 +629,13 @@ packages: description: flutter source: sdk 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: dependency: transitive description: @@ -637,6 +674,13 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.2" freezed_annotation: dependency: transitive description: @@ -826,6 +870,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.6" + mailer: + dependency: transitive + description: + name: mailer + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" marquee: dependency: "direct main" description: @@ -902,42 +953,14 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.4.3+1" - 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" + version: "3.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" - 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" + version: "2.0.1" palette_generator: dependency: "direct main" description: @@ -1185,6 +1208,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + sentry: + dependency: transitive + description: + name: sentry + url: "https://pub.dartlang.org" + source: hosted + version: "6.19.0" shared_preferences: dependency: "direct main" description: @@ -1393,6 +1423,13 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ffe722eb..79c46ae2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,9 @@ dependencies: auto_size_text: ^3.0.0 badges: ^2.0.3 cached_network_image: ^3.2.2 + catcher: + git: + url: https://github.com/ThexXTURBOXx/catcher collection: ^1.15.0 cupertino_icons: ^1.0.5 dbus: ^0.7.8 @@ -47,7 +50,7 @@ dependencies: marquee: ^2.2.3 metadata_god: ^0.3.2 mime: ^1.0.2 - package_info_plus: ^1.4.3 + package_info_plus: ^3.0.2 palette_generator: ^0.3.3 path: ^1.8.0 path_provider: ^2.0.8 @@ -85,6 +88,9 @@ dev_dependencies: sdk: flutter hive_generator: ^2.0.0 +dependency_overrides: + package_info_plus: ^3.0.2 + flutter: uses-material-design: true assets: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index a39133cb..13b352c5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + CatcherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CatcherPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); MetadataGodPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 86f316df..dfab40f7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows + catcher connectivity_plus_windows metadata_god permission_handler_windows