mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
105ee469e8
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user