mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
ranking based synced lyrics selection for better accuracy
This commit is contained in:
parent
8af0281b23
commit
dc9b09f496
@ -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],
|
||||||
);
|
);
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
32
lib/models/SpotubeTrack.dart
Normal file
32
lib/models/SpotubeTrack.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user