From f3bacad2334a1283d105debb5675a7608f372e82 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 Jun 2022 22:27:11 +0600 Subject: [PATCH] Customizable Track Match Algorigthm AudioQuality Option --- lib/components/Home/Home.dart | 2 +- lib/components/Settings/Settings.dart | 54 ++++++++++++ lib/helpers/search-youtube.dart | 120 ++++++++++++++------------ lib/models/SpotubeTrack.dart | 9 ++ lib/provider/Playback.dart | 27 ++++-- lib/provider/UserPreferences.dart | 27 +++++- 6 files changed, 174 insertions(+), 65 deletions(-) diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index 63f849b3..b4968d7f 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -43,7 +43,7 @@ class Home extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final int titleBarDragMaxWidth = useBreakpointValue( - md: 72, + md: 80, lg: 256, sm: 0, xl: 0, diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 1fa499b4..27179dbd 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -9,7 +9,9 @@ import 'package:spotube/components/Settings/About.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/models/SpotifyMarkets.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -201,6 +203,58 @@ class Settings extends HookConsumerWidget { preferences.setCheckUpdate(checked), ), ), + ListTile( + title: const Text("Track Match Algorithm"), + trailing: DropdownButton( + value: preferences.trackMatchAlgorithm, + items: const [ + DropdownMenuItem( + child: Text( + "Popular from Author", + ), + value: SpotubeTrackMatchAlgorithm.authenticPopular, + ), + DropdownMenuItem( + child: Text( + "Accurately Popular", + ), + value: SpotubeTrackMatchAlgorithm.popular, + ), + DropdownMenuItem( + child: Text("YouTube's choice is my choice"), + value: SpotubeTrackMatchAlgorithm.youtube, + ), + ], + onChanged: (value) { + if (value != null) { + preferences.setTrackMatchAlgorithm(value); + } + }, + ), + ), + ListTile( + title: const Text("Audio Quality"), + trailing: DropdownButton( + value: preferences.audioQuality, + items: const [ + DropdownMenuItem( + child: Text( + "High", + ), + value: AudioQuality.high, + ), + DropdownMenuItem( + child: Text("Low"), + value: AudioQuality.low, + ), + ], + onChanged: (value) { + if (value != null) { + preferences.setAudioQuality(value); + } + }, + ), + ), if (auth.isLoggedIn) Builder(builder: (context) { Auth auth = ref.watch(authProvider); diff --git a/lib/helpers/search-youtube.dart b/lib/helpers/search-youtube.dart index 5d449144..9e1975b9 100644 --- a/lib/helpers/search-youtube.dart +++ b/lib/helpers/search-youtube.dart @@ -8,9 +8,19 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:collection/collection.dart'; import 'package:spotube/extensions/list-sort-multiple.dart'; +enum AudioQuality { + high, + low, +} + final logger = getLogger("toSpotubeTrack"); -Future toSpotubeTrack( - YoutubeExplode youtube, Track track, String format) async { +Future toSpotubeTrack({ + required YoutubeExplode youtube, + required Track track, + required String format, + required SpotubeTrackMatchAlgorithm matchAlgorithm, + required AudioQuality audioQuality, +}) async { final artistsName = track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? []; @@ -31,60 +41,53 @@ Future toSpotubeTrack( logger.v("[Youtube Search Term] $queryString"); VideoSearchList videos = await youtube.search.search(queryString); + Video ytVideo; - 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(); + 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 = !containsTextInBracket(ytTitle, "live"); + final bool hasNoLiveInTitle = !containsTextInBracket(ytTitle, "live"); - // final bool hasOfficialVideo = [ - // "(official video)", - // "[official video]", - // "(official music video)", - // "[official music video]" - // ].any((v) => ytTitle.contains(v)); + int rate = 0; + for (final el in [ + hasTitle, + hasAllArtists, + if (matchAlgorithm == SpotubeTrackMatchAlgorithm.authenticPopular) + authorIsArtist, + hasNoLiveInTitle, + !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"], + ); - // final bool hasOfficialAudio = [ - // "[official audio]", - // "(official audio)", - // ].any((v) => ytTitle.contains(v)); - - int rate = 0; - for (final el in [ - hasTitle, - hasAllArtists, - authorIsArtist, - hasNoLiveInTitle, - !video.isLive, - // hasOfficialVideo, - // hasOfficialAudio, - ]) { - 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"], - ); - - final ytVideo = ratedRankedVideos.first["video"] as Video; + ytVideo = ratedRankedVideos.first["video"] as Video; + } else { + ytVideo = videos.where((video) => !video.isLive).first; + } final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id); @@ -92,17 +95,20 @@ Future toSpotubeTrack( "[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}", ); + final audioManifest = (Platform.isMacOS || Platform.isIOS) + ? trackManifest.audioOnly + .where((info) => info.codec.mimeType == "audio/mp4") + : trackManifest.audioOnly; + return SpotubeTrack.fromTrack( track: track, ytTrack: ytVideo, // Since Mac OS's & IOS's CodeAudio doesn't support WebMedia // ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg' // codec/mimetype for those Platforms - ytUri: (Platform.isMacOS || Platform.isIOS - ? trackManifest.audioOnly - .where((info) => info.codec.mimeType == "audio/mp4") - .withHighestBitrate() - : trackManifest.audioOnly.withHighestBitrate()) + ytUri: (audioQuality == AudioQuality.high + ? audioManifest.withHighestBitrate() + : audioManifest.sortByBitrate().last) .url .toString(), ); diff --git a/lib/models/SpotubeTrack.dart b/lib/models/SpotubeTrack.dart index 70adf5f2..a1edaaaa 100644 --- a/lib/models/SpotubeTrack.dart +++ b/lib/models/SpotubeTrack.dart @@ -1,6 +1,15 @@ import 'package:spotify/spotify.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; +enum SpotubeTrackMatchAlgorithm { + // selects the first result returned from YouTube + youtube, + // selects the most popular one + popular, + // selects the most popular one from the author of the track + authenticPopular, +} + class SpotubeTrack extends Track { Video ytTrack; String ytUri; diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index de15ae6f..13276b7a 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -48,15 +48,19 @@ class Playback extends ChangeNotifier { _init(); } + StreamSubscription? _durationStream; + StreamSubscription? _positionStream; + StreamSubscription? _playingStream; + void _init() { - player.core.playingStream.listen( + _playingStream = player.core.playingStream.listen( (playing) { _isPlaying = playing; notifyListeners(); }, ); - player.core.durationStream.listen((event) async { + _durationStream = player.core.durationStream.listen((event) async { if (event != null) { // Actually things doesn't work all the time as they were // described. So instead of listening to a `_ready` @@ -73,7 +77,8 @@ class Playback extends ChangeNotifier { } }); - player.core.createPositionStream().listen((position) async { + _positionStream = + player.core.createPositionStream().listen((position) async { // detecting multiple same call if (_prevPosition.inSeconds == position.inSeconds) return; _prevPosition = position; @@ -97,6 +102,14 @@ class Playback extends ChangeNotifier { }); } + @override + void dispose() { + _positionStream?.cancel(); + _playingStream?.cancel(); + _durationStream?.cancel(); + super.dispose(); + } + bool get shuffled => _shuffled; CurrentPlaylist? get currentPlaylist => _currentPlaylist; Track? get currentTrack => _currentTrack; @@ -194,9 +207,11 @@ class Playback extends ChangeNotifier { return; } final spotubeTrack = await toSpotubeTrack( - youtube, - track, - preferences.ytSearchFormat, + youtube: youtube, + track: track, + format: preferences.ytSearchFormat, + matchAlgorithm: preferences.trackMatchAlgorithm, + audioQuality: preferences.audioQuality, ); if (setTrackUriById(track.id!, spotubeTrack.ytUri)) { _currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri)); diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index 701b8517..55780d7e 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/helpers/get-random-element.dart'; +import 'package:spotube/helpers/search-youtube.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/utils/PersistedChangeNotifier.dart'; import 'package:collection/collection.dart'; @@ -19,8 +21,9 @@ class UserPreferences extends PersistedChangeNotifier { HotKey? nextTrackHotKey; HotKey? prevTrackHotKey; HotKey? playPauseHotKey; - bool checkUpdate; + SpotubeTrackMatchAlgorithm trackMatchAlgorithm; + AudioQuality audioQuality; MaterialColor accentColorScheme; MaterialColor backgroundColorScheme; @@ -36,6 +39,8 @@ class UserPreferences extends PersistedChangeNotifier { this.prevTrackHotKey, this.playPauseHotKey, this.checkUpdate = true, + this.trackMatchAlgorithm = SpotubeTrackMatchAlgorithm.authenticPopular, + this.audioQuality = AudioQuality.high, }) : super(); void setThemeMode(ThemeMode mode) { @@ -104,6 +109,18 @@ class UserPreferences extends PersistedChangeNotifier { updatePersistence(); } + void setTrackMatchAlgorithm(SpotubeTrackMatchAlgorithm algorithm) { + trackMatchAlgorithm = algorithm; + notifyListeners(); + updatePersistence(); + } + + void setAudioQuality(AudioQuality quality) { + audioQuality = quality; + notifyListeners(); + updatePersistence(); + } + @override FutureOr loadFromLocal(Map map) { saveTrackLyrics = map["saveTrackLyrics"] ?? false; @@ -128,6 +145,12 @@ class UserPreferences extends PersistedChangeNotifier { accentColorScheme = colorsMap.values .firstWhereOrNull((e) => e.value == map["accentColorScheme"]) ?? accentColorScheme; + trackMatchAlgorithm = map["trackMatchAlgorithm"] != null + ? SpotubeTrackMatchAlgorithm.values[map["trackMatchAlgorithm"]] + : trackMatchAlgorithm; + audioQuality = map["audioQuality"] != null + ? AudioQuality.values[map["audioQuality"]] + : audioQuality; } @override @@ -150,6 +173,8 @@ class UserPreferences extends PersistedChangeNotifier { "backgroundColorScheme": backgroundColorScheme.value, "accentColorScheme": accentColorScheme.value, "checkUpdate": checkUpdate, + "trackMatchAlgorithm": trackMatchAlgorithm.index, + "audioQuality": audioQuality.index, }; } }