ranking based synced lyrics selection for better accuracy

This commit is contained in:
Kingkor Roy Tirtho 2022-04-21 10:20:11 +06:00
parent 8af0281b23
commit dc9b09f496
6 changed files with 90 additions and 33 deletions

View File

@ -7,6 +7,7 @@ import 'package:spotube/helpers/timed-lyrics.dart';
import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useAutoScrollController.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useSyncedLyrics.dart'; import 'package:spotube/hooks/useSyncedLyrics.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
@ -19,15 +20,17 @@ class SyncedLyrics extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final timedLyrics = useMemoized(() { final timedLyrics = useMemoized(() {
if (playback.currentTrack == null) return null; if (playback.currentTrack == null ||
return getTimedLyrics(playback.currentTrack!); playback.currentTrack is! SpotubeTrack) return null;
return getTimedLyrics(playback.currentTrack as SpotubeTrack);
}, [playback.currentTrack]); }, [playback.currentTrack]);
final lyricsSnapshot = useFuture(timedLyrics); final lyricsSnapshot = useFuture(timedLyrics);
final lyricsMap = useMemoized( final lyricsMap = useMemoized(
() => () =>
lyricsSnapshot.data?.lyrics lyricsSnapshot.data?.lyrics
.map((lyric) => {lyric.time.inSeconds: lyric.text}) .map((lyric) => {lyric.time.inSeconds: lyric.text})
.reduce((a, b) => {...a, ...b}) ?? .reduce((accumulator, lyricSlice) =>
{...accumulator, ...lyricSlice}) ??
{}, {},
[lyricsSnapshot.data], [lyricsSnapshot.data],
); );

View File

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
@ -42,8 +43,8 @@ class DownloadTrackButton extends HookConsumerWidget {
return; return;
} }
} }
StreamManifest manifest = StreamManifest manifest = await yt.videos.streamsClient
await yt.videos.streamsClient.getManifest(track?.href); .getManifest((track as SpotubeTrack).ytTrack.url);
String downloadFolder = path.join( String downloadFolder = path.join(
Platform.isAndroid Platform.isAndroid
@ -177,10 +178,7 @@ class DownloadTrackButton extends HookConsumerWidget {
} }
return IconButton( return IconButton(
icon: const Icon(Icons.download_rounded), icon: const Icon(Icons.download_rounded),
onPressed: track != null && onPressed: track != null && track is SpotubeTrack ? _downloadTrack : null,
!(track!.href ?? "").startsWith("https://api.spotify.com")
? _downloadTrack
: null,
); );
} }
} }

View File

@ -2,12 +2,13 @@ import 'dart:io';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; 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';
final logger = getLogger("toYoutubeTrack"); final logger = getLogger("toSpotubeTrack");
Future<Track> toYoutubeTrack( Future<SpotubeTrack> toSpotubeTrack(
YoutubeExplode youtube, Track track, String format) async { YoutubeExplode youtube, Track track, String format) async {
final artistsName = final artistsName =
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
@ -62,18 +63,22 @@ Future<Track> toYoutubeTrack(
final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id); 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( logger.v(
"[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${track.href}"); "[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}",
return track; );
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(),
);
} }

View File

@ -1,7 +1,9 @@
import 'package:html/dom.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:spotify/spotify.dart'; import 'package:collection/collection.dart';
import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/models/SpotubeTrack.dart';
class SubtitleSimple { class SubtitleSimple {
Uri uri; Uri uri;
@ -28,7 +30,7 @@ class LyricSlice {
const baseUri = "https://www.rentanadviser.com/subtitles"; const baseUri = "https://www.rentanadviser.com/subtitles";
Future<SubtitleSimple?> getTimedLyrics(Track track) async { Future<SubtitleSimple?> getTimedLyrics(SpotubeTrack track) async {
final artistNames = final artistNames =
track.artists?.map((artist) => artist.name!).toList() ?? []; track.artists?.map((artist) => artist.name!).toList() ?? [];
final query = getTitle( final query = getTitle(
@ -41,10 +43,27 @@ Future<SubtitleSimple?> getTimedLyrics(Track track) async {
final res = await http.get(searchUri); final res = await http.get(searchUri);
final document = parse(res.body); final document = parse(res.body);
final topResult = final results =
document.querySelector("#tablecontainer table tbody tr td a"); 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 = final subtitleUri =
Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc"); Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc");

View File

@ -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;
}
}

View File

@ -274,21 +274,21 @@ class Playback extends ChangeNotifier {
}); });
} }
final preferences = ref.read(userPreferencesProvider); final preferences = ref.read(userPreferencesProvider);
final ytTrack = await toYoutubeTrack( final spotubeTrack = await toSpotubeTrack(
youtube, youtube,
track, track,
preferences.ytSearchFormat, preferences.ytSearchFormat,
); );
if (setTrackUriById(track.id!, ytTrack.uri!)) { if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
_currentAudioSource = _currentAudioSource =
AudioSource.uri(Uri.parse(ytTrack.uri!), tag: tag); AudioSource.uri(Uri.parse(spotubeTrack.ytUri), tag: tag);
await player await player
.setAudioSource( .setAudioSource(
_currentAudioSource!, _currentAudioSource!,
preload: true, preload: true,
) )
.then((value) { .then((value) {
_currentTrack = track; _currentTrack = spotubeTrack;
notifyListeners(); notifyListeners();
}); });
} }