From dc9b09f496b8fc135fe0e9a0786911558d1f40e6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 21 Apr 2022 10:20:11 +0600 Subject: [PATCH] ranking based synced lyrics selection for better accuracy --- lib/components/Lyrics/SyncedLyrics.dart | 9 +++-- .../Shared/DownloadTrackButton.dart | 10 +++--- lib/helpers/search-youtube.dart | 35 +++++++++++-------- lib/helpers/timed-lyrics.dart | 29 ++++++++++++--- lib/models/SpotubeTrack.dart | 32 +++++++++++++++++ lib/provider/Playback.dart | 8 ++--- 6 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 lib/models/SpotubeTrack.dart diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index 647dfa8a..a35cc5f2 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -7,6 +7,7 @@ import 'package:spotube/helpers/timed-lyrics.dart'; import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useSyncedLyrics.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; @@ -19,15 +20,17 @@ class SyncedLyrics extends HookConsumerWidget { final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); final timedLyrics = useMemoized(() { - if (playback.currentTrack == null) return null; - return getTimedLyrics(playback.currentTrack!); + if (playback.currentTrack == null || + playback.currentTrack is! SpotubeTrack) return null; + return getTimedLyrics(playback.currentTrack as SpotubeTrack); }, [playback.currentTrack]); final lyricsSnapshot = useFuture(timedLyrics); final lyricsMap = useMemoized( () => lyricsSnapshot.data?.lyrics .map((lyric) => {lyric.time.inSeconds: lyric.text}) - .reduce((a, b) => {...a, ...b}) ?? + .reduce((accumulator, lyricSlice) => + {...accumulator, ...lyricSlice}) ?? {}, [lyricsSnapshot.data], ); diff --git a/lib/components/Shared/DownloadTrackButton.dart b/lib/components/Shared/DownloadTrackButton.dart index 22e8874e..d085b6d0 100644 --- a/lib/components/Shared/DownloadTrackButton.dart +++ b/lib/components/Shared/DownloadTrackButton.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/getLyrics.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; @@ -42,8 +43,8 @@ class DownloadTrackButton extends HookConsumerWidget { return; } } - StreamManifest manifest = - await yt.videos.streamsClient.getManifest(track?.href); + StreamManifest manifest = await yt.videos.streamsClient + .getManifest((track as SpotubeTrack).ytTrack.url); String downloadFolder = path.join( Platform.isAndroid @@ -177,10 +178,7 @@ class DownloadTrackButton extends HookConsumerWidget { } return IconButton( icon: const Icon(Icons.download_rounded), - onPressed: track != null && - !(track!.href ?? "").startsWith("https://api.spotify.com") - ? _downloadTrack - : null, + onPressed: track != null && track is SpotubeTrack ? _downloadTrack : null, ); } } diff --git a/lib/helpers/search-youtube.dart b/lib/helpers/search-youtube.dart index b4318847..c5cbb336 100644 --- a/lib/helpers/search-youtube.dart +++ b/lib/helpers/search-youtube.dart @@ -2,12 +2,13 @@ import 'dart:io'; import 'package:spotify/spotify.dart'; import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/models/Logger.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:collection/collection.dart'; import 'package:spotube/extensions/list-sort-multiple.dart'; -final logger = getLogger("toYoutubeTrack"); -Future toYoutubeTrack( +final logger = getLogger("toSpotubeTrack"); +Future toSpotubeTrack( YoutubeExplode youtube, Track track, String format) async { final artistsName = track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? @@ -62,18 +63,22 @@ Future toYoutubeTrack( final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id); - // 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 - track.uri = (Platform.isMacOS || Platform.isIOS - ? trackManifest.audioOnly - .where((info) => info.codec.mimeType == "audio/mp4") - .withHighestBitrate() - : trackManifest.audioOnly.withHighestBitrate()) - .url - .toString(); - track.href = ytVideo.url; logger.v( - "[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${track.href}"); - return track; + "[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}", + ); + + 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()) + .url + .toString(), + ); } diff --git a/lib/helpers/timed-lyrics.dart b/lib/helpers/timed-lyrics.dart index 45fb66d5..0db44bc3 100644 --- a/lib/helpers/timed-lyrics.dart +++ b/lib/helpers/timed-lyrics.dart @@ -1,7 +1,9 @@ +import 'package:html/dom.dart'; import 'package:http/http.dart' as http; import 'package:html/parser.dart'; -import 'package:spotify/spotify.dart'; +import 'package:collection/collection.dart'; import 'package:spotube/helpers/getLyrics.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; class SubtitleSimple { Uri uri; @@ -28,7 +30,7 @@ class LyricSlice { const baseUri = "https://www.rentanadviser.com/subtitles"; -Future getTimedLyrics(Track track) async { +Future getTimedLyrics(SpotubeTrack track) async { final artistNames = track.artists?.map((artist) => artist.name!).toList() ?? []; final query = getTitle( @@ -41,10 +43,27 @@ Future getTimedLyrics(Track track) async { final res = await http.get(searchUri); final document = parse(res.body); - final topResult = - document.querySelector("#tablecontainer table tbody tr td a"); + final results = + document.querySelectorAll("#tablecontainer table tbody tr td a"); - if (topResult == null) return null; + final topResult = results + .map((result) { + final title = result.text.trim().toLowerCase(); + int points = 0; + final hasAllArtists = track.artists + ?.map((artist) => artist.name!) + .every((artist) => title.contains(artist.toLowerCase())) ?? + false; + final hasTrackName = title.contains(track.name!.toLowerCase()); + final exactYtMatch = title == track.ytTrack.title.toLowerCase(); + if (exactYtMatch) points = 8; + for (final criteria in [hasTrackName, hasAllArtists]) { + if (criteria) points++; + } + return {"result": result, "points": points}; + }) + .sorted((a, b) => (b["points"] as int).compareTo(a["points"] as int)) + .first["result"] as Element; final subtitleUri = Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc"); diff --git a/lib/models/SpotubeTrack.dart b/lib/models/SpotubeTrack.dart new file mode 100644 index 00000000..70adf5f2 --- /dev/null +++ b/lib/models/SpotubeTrack.dart @@ -0,0 +1,32 @@ +import 'package:spotify/spotify.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +class SpotubeTrack extends Track { + Video ytTrack; + String ytUri; + + SpotubeTrack.fromTrack({ + required Track track, + required this.ytTrack, + required this.ytUri, + }) { + album = track.album; + artists = track.artists; + availableMarkets = track.availableMarkets; + discNumber = track.discNumber; + durationMs = track.durationMs; + explicit = track.explicit; + externalIds = track.externalIds; + externalUrls = track.externalUrls; + href = track.href; + id = track.id; + isPlayable = track.isPlayable; + linkedFrom = track.linkedFrom; + name = track.name; + popularity = track.popularity; + previewUrl = track.previewUrl; + trackNumber = track.trackNumber; + type = track.type; + uri = track.uri; + } +} diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 6da9b002..d327fcec 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -274,21 +274,21 @@ class Playback extends ChangeNotifier { }); } final preferences = ref.read(userPreferencesProvider); - final ytTrack = await toYoutubeTrack( + final spotubeTrack = await toSpotubeTrack( youtube, track, preferences.ytSearchFormat, ); - if (setTrackUriById(track.id!, ytTrack.uri!)) { + if (setTrackUriById(track.id!, spotubeTrack.ytUri)) { _currentAudioSource = - AudioSource.uri(Uri.parse(ytTrack.uri!), tag: tag); + AudioSource.uri(Uri.parse(spotubeTrack.ytUri), tag: tag); await player .setAudioSource( _currentAudioSource!, preload: true, ) .then((value) { - _currentTrack = track; + _currentTrack = spotubeTrack; notifyListeners(); }); }