Customizable Track Match Algorigthm

AudioQuality Option
This commit is contained in:
Kingkor Roy Tirtho 2022-06-04 22:27:11 +06:00
parent b3b3acdb1e
commit f3bacad233
6 changed files with 174 additions and 65 deletions

View File

@ -43,7 +43,7 @@ class Home extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final int titleBarDragMaxWidth = useBreakpointValue( final int titleBarDragMaxWidth = useBreakpointValue(
md: 72, md: 80,
lg: 256, lg: 256,
sm: 0, sm: 0,
xl: 0, xl: 0,

View File

@ -9,7 +9,9 @@ import 'package:spotube/components/Settings/About.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.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/SpotifyMarkets.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -201,6 +203,58 @@ class Settings extends HookConsumerWidget {
preferences.setCheckUpdate(checked), preferences.setCheckUpdate(checked),
), ),
), ),
ListTile(
title: const Text("Track Match Algorithm"),
trailing: DropdownButton<SpotubeTrackMatchAlgorithm>(
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<AudioQuality>(
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) if (auth.isLoggedIn)
Builder(builder: (context) { Builder(builder: (context) {
Auth auth = ref.watch(authProvider); Auth auth = ref.watch(authProvider);

View File

@ -8,9 +8,19 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:spotube/extensions/list-sort-multiple.dart'; import 'package:spotube/extensions/list-sort-multiple.dart';
enum AudioQuality {
high,
low,
}
final logger = getLogger("toSpotubeTrack"); final logger = getLogger("toSpotubeTrack");
Future<SpotubeTrack> toSpotubeTrack( Future<SpotubeTrack> toSpotubeTrack({
YoutubeExplode youtube, Track track, String format) async { required YoutubeExplode youtube,
required Track track,
required String format,
required SpotubeTrackMatchAlgorithm matchAlgorithm,
required AudioQuality audioQuality,
}) async {
final artistsName = final artistsName =
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
[]; [];
@ -31,60 +41,53 @@ Future<SpotubeTrack> toSpotubeTrack(
logger.v("[Youtube Search Term] $queryString"); logger.v("[Youtube Search Term] $queryString");
VideoSearchList videos = await youtube.search.search(queryString); VideoSearchList videos = await youtube.search.search(queryString);
Video ytVideo;
List<Map> ratedRankedVideos = videos if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
.map((video) { List<Map> ratedRankedVideos = videos
// the find should be lazy thus everything case insensitive .map((video) {
final ytTitle = video.title.toLowerCase(); // the find should be lazy thus everything case insensitive
final bool hasTitle = ytTitle.contains(title); final ytTitle = video.title.toLowerCase();
final bool hasAllArtists = track.artists?.every( final bool hasTitle = ytTitle.contains(title);
(artist) => ytTitle.contains(artist.name!.toLowerCase()), final bool hasAllArtists = track.artists?.every(
) ?? (artist) => ytTitle.contains(artist.name!.toLowerCase()),
false; ) ??
final bool authorIsArtist = track.artists?.first.name?.toLowerCase() == false;
video.author.toLowerCase(); 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 = [ int rate = 0;
// "(official video)", for (final el in [
// "[official video]", hasTitle,
// "(official music video)", hasAllArtists,
// "[official music video]" if (matchAlgorithm == SpotubeTrackMatchAlgorithm.authenticPopular)
// ].any((v) => ytTitle.contains(v)); 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 = [ ytVideo = ratedRankedVideos.first["video"] as Video;
// "[official audio]", } else {
// "(official audio)", ytVideo = videos.where((video) => !video.isLive).first;
// ].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;
final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id); final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
@ -92,17 +95,20 @@ Future<SpotubeTrack> toSpotubeTrack(
"[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}", "[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( return SpotubeTrack.fromTrack(
track: track, track: track,
ytTrack: ytVideo, ytTrack: ytVideo,
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia // Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg' // ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
// codec/mimetype for those Platforms // codec/mimetype for those Platforms
ytUri: (Platform.isMacOS || Platform.isIOS ytUri: (audioQuality == AudioQuality.high
? trackManifest.audioOnly ? audioManifest.withHighestBitrate()
.where((info) => info.codec.mimeType == "audio/mp4") : audioManifest.sortByBitrate().last)
.withHighestBitrate()
: trackManifest.audioOnly.withHighestBitrate())
.url .url
.toString(), .toString(),
); );

View File

@ -1,6 +1,15 @@
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.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 { class SpotubeTrack extends Track {
Video ytTrack; Video ytTrack;
String ytUri; String ytUri;

View File

@ -48,15 +48,19 @@ class Playback extends ChangeNotifier {
_init(); _init();
} }
StreamSubscription<Duration?>? _durationStream;
StreamSubscription<Duration>? _positionStream;
StreamSubscription<bool>? _playingStream;
void _init() { void _init() {
player.core.playingStream.listen( _playingStream = player.core.playingStream.listen(
(playing) { (playing) {
_isPlaying = playing; _isPlaying = playing;
notifyListeners(); notifyListeners();
}, },
); );
player.core.durationStream.listen((event) async { _durationStream = player.core.durationStream.listen((event) async {
if (event != null) { if (event != null) {
// Actually things doesn't work all the time as they were // Actually things doesn't work all the time as they were
// described. So instead of listening to a `_ready` // 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 // detecting multiple same call
if (_prevPosition.inSeconds == position.inSeconds) return; if (_prevPosition.inSeconds == position.inSeconds) return;
_prevPosition = position; _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; bool get shuffled => _shuffled;
CurrentPlaylist? get currentPlaylist => _currentPlaylist; CurrentPlaylist? get currentPlaylist => _currentPlaylist;
Track? get currentTrack => _currentTrack; Track? get currentTrack => _currentTrack;
@ -194,9 +207,11 @@ class Playback extends ChangeNotifier {
return; return;
} }
final spotubeTrack = await toSpotubeTrack( final spotubeTrack = await toSpotubeTrack(
youtube, youtube: youtube,
track, track: track,
preferences.ytSearchFormat, format: preferences.ytSearchFormat,
matchAlgorithm: preferences.trackMatchAlgorithm,
audioQuality: preferences.audioQuality,
); );
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) { if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
_currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri)); _currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri));

View File

@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/helpers/get-random-element.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/models/generated_secrets.dart';
import 'package:spotube/utils/PersistedChangeNotifier.dart'; import 'package:spotube/utils/PersistedChangeNotifier.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -19,8 +21,9 @@ class UserPreferences extends PersistedChangeNotifier {
HotKey? nextTrackHotKey; HotKey? nextTrackHotKey;
HotKey? prevTrackHotKey; HotKey? prevTrackHotKey;
HotKey? playPauseHotKey; HotKey? playPauseHotKey;
bool checkUpdate; bool checkUpdate;
SpotubeTrackMatchAlgorithm trackMatchAlgorithm;
AudioQuality audioQuality;
MaterialColor accentColorScheme; MaterialColor accentColorScheme;
MaterialColor backgroundColorScheme; MaterialColor backgroundColorScheme;
@ -36,6 +39,8 @@ class UserPreferences extends PersistedChangeNotifier {
this.prevTrackHotKey, this.prevTrackHotKey,
this.playPauseHotKey, this.playPauseHotKey,
this.checkUpdate = true, this.checkUpdate = true,
this.trackMatchAlgorithm = SpotubeTrackMatchAlgorithm.authenticPopular,
this.audioQuality = AudioQuality.high,
}) : super(); }) : super();
void setThemeMode(ThemeMode mode) { void setThemeMode(ThemeMode mode) {
@ -104,6 +109,18 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence(); updatePersistence();
} }
void setTrackMatchAlgorithm(SpotubeTrackMatchAlgorithm algorithm) {
trackMatchAlgorithm = algorithm;
notifyListeners();
updatePersistence();
}
void setAudioQuality(AudioQuality quality) {
audioQuality = quality;
notifyListeners();
updatePersistence();
}
@override @override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) { FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
saveTrackLyrics = map["saveTrackLyrics"] ?? false; saveTrackLyrics = map["saveTrackLyrics"] ?? false;
@ -128,6 +145,12 @@ class UserPreferences extends PersistedChangeNotifier {
accentColorScheme = colorsMap.values accentColorScheme = colorsMap.values
.firstWhereOrNull((e) => e.value == map["accentColorScheme"]) ?? .firstWhereOrNull((e) => e.value == map["accentColorScheme"]) ??
accentColorScheme; accentColorScheme;
trackMatchAlgorithm = map["trackMatchAlgorithm"] != null
? SpotubeTrackMatchAlgorithm.values[map["trackMatchAlgorithm"]]
: trackMatchAlgorithm;
audioQuality = map["audioQuality"] != null
? AudioQuality.values[map["audioQuality"]]
: audioQuality;
} }
@override @override
@ -150,6 +173,8 @@ class UserPreferences extends PersistedChangeNotifier {
"backgroundColorScheme": backgroundColorScheme.value, "backgroundColorScheme": backgroundColorScheme.value,
"accentColorScheme": accentColorScheme.value, "accentColorScheme": accentColorScheme.value,
"checkUpdate": checkUpdate, "checkUpdate": checkUpdate,
"trackMatchAlgorithm": trackMatchAlgorithm.index,
"audioQuality": audioQuality.index,
}; };
} }
} }