From a0a1b88ce24d7d0d227eea4ed5199890b407f9ed Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Thu, 22 Feb 2024 01:11:15 +0530 Subject: [PATCH 01/10] Added azLyrics query Helps to fetch lyrics from azlyrics website --- lib/services/queries/lyrics.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index 618f960fd..bc22fbe4e 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -15,6 +15,20 @@ import 'package:http/http.dart' as http; class LyricsQueries { const LyricsQueries(); + Query azLyrics(Track? track) { + return useQuery("azlyrics-query/${track?.id}", () async { + if (track == null) { + throw "No Track Currently"; + } + final lyrics = await ServiceUtils.getAZLyrics( + title: track.name!, + artists: + track.artists?.map((s) => s.name).whereNotNull().toList() ?? []); + return lyrics; + }); + } + + Query static( Track? track, String geniusAccessToken, From f5438cf04a0f8d9837fe4300d88f38c5d6bfef89 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Thu, 22 Feb 2024 01:15:24 +0530 Subject: [PATCH 02/10] Update service_utils.dart --- lib/utils/service_utils.dart | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 9e3b58934..2ddfe7fb6 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -119,6 +119,51 @@ abstract class ServiceUtils { return results; } + static Future getAZLyrics( + {required String title, required List artists}) async { + //Couldn't figure out a way to generate value for x. Also, it remains the same across different IP addresses. + final suggestionUrl = Uri.parse( + "https://search.azlyrics.com/suggest.php?q=$title ${artists[0]}&x=884911ec808d4712b839f06754f62ef23cddd06a36e86bf8d44fbd2bac3e6a56"); + + const Map headers = { + HttpHeaders.userAgentHeader: + "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600" + }; + final searchResponse = await http.get(suggestionUrl, headers: headers); + + if (searchResponse.statusCode != 200) { + throw "searchResponse = ${searchResponse.statusCode}"; + } + + final Map searchResult = jsonDecode(searchResponse.body); + + String bestLyricsURL; + + try { + bestLyricsURL = searchResult["songs"][0]["url"]; + debugPrint("getAZLyrics -> bestLyricsURL: $bestLyricsURL"); + } catch (e) { + throw "No best Lyrics URL"; + } + + final lyricsResponse = + await http.get(Uri.parse(bestLyricsURL), headers: headers); + + if (lyricsResponse.statusCode != 200) { + throw "lyricsResponse = ${lyricsResponse.statusCode}"; + } + + var document = parser.parse(lyricsResponse.body); + var lyricsDiv = document.querySelectorAll( + "body > div.container.main-page > div.row > div.col-xs-12.col-lg-8.text-center > div"); + + if (lyricsDiv.isEmpty) throw "lyricsDiv is empty"; + + final String lyrics = lyricsDiv[4].text; + + return lyrics; + } + @Deprecated("In favor spotify lyrics api, this isn't needed anymore") static Future getLyrics( String title, From 9259e61367b04b4d0c5740329943817c9d88e5e1 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Thu, 22 Feb 2024 01:38:03 +0530 Subject: [PATCH 03/10] Added method to fetch unsynced lyrics from azlyrics as a fallback. Spotify lyrics for many songs are not available for non-premium users. Now, it will fetch unsynced lyrics from Azlyrics as a fallback. --- lib/pages/lyrics/plain_lyrics.dart | 66 +++++++++++++++++------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index bee5114d4..937b54ad8 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -32,10 +32,12 @@ class PlainLyrics extends HookConsumerWidget { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final lyricsQuery = useQueries.lyrics.spotifySynced(ref, playlist.activeTrack); + final azLyricsQuery = useQueries.lyrics.azLyrics(playlist.activeTrack); final mediaQuery = MediaQuery.of(context); final textTheme = Theme.of(context).textTheme; final textZoomLevel = useState(defaultTextZoom); + bool useAZLyrics = false; return Stack( children: [ @@ -75,38 +77,46 @@ class PlainLyrics extends HookConsumerWidget { if (lyricsQuery.isLoading || lyricsQuery.isRefreshing) { return const ShimmerLyrics(); } else if (lyricsQuery.hasError) { - return Container( - alignment: Alignment.center, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.no_lyrics_available, - style: textTheme.bodyLarge?.copyWith( - color: palette.bodyTextColor, + if (azLyricsQuery.isLoading || + azLyricsQuery.isRefreshing) { + return const ShimmerLyrics(); + } else if (azLyricsQuery.hasError) { + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.no_lyrics_available, + style: textTheme.bodyLarge?.copyWith( + color: palette.bodyTextColor, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - const Gap(26), - const Icon(SpotubeIcons.noLyrics, size: 60), - ], - ), - ); + const Gap(26), + const Icon(SpotubeIcons.noLyrics, size: 60), + ], + ), + ); + } else { + useAZLyrics = true; + } } - final lyrics = - lyricsQuery.data?.lyrics.mapIndexed((i, e) { - final next = - lyricsQuery.data?.lyrics.elementAtOrNull(i + 1); - if (next != null && - e.time - next.time > - const Duration(milliseconds: 700)) { - return "${e.text}\n"; - } + final lyrics = !useAZLyrics + ? lyricsQuery.data?.lyrics.mapIndexed((i, e) { + final next = lyricsQuery.data?.lyrics + .elementAtOrNull(i + 1); + if (next != null && + e.time - next.time > + const Duration(milliseconds: 700)) { + return "${e.text}\n"; + } - return e.text; - }).join("\n"); + return e.text; + }).join("\n") + : azLyricsQuery.data; return AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), From ad13f99525fcbccf67605420810fa255c62144ed Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Thu, 22 Feb 2024 13:44:16 +0530 Subject: [PATCH 04/10] Fixed the AZLyrics URL and trimmed the lyrics Now it will adapt to dynamic changes in the URL parameters. Additionally, it will remove extra spaces from the beginning of the lyrics. --- lib/utils/service_utils.dart | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 2ddfe7fb6..84f38f6a5 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -121,16 +121,35 @@ abstract class ServiceUtils { static Future getAZLyrics( {required String title, required List artists}) async { - //Couldn't figure out a way to generate value for x. Also, it remains the same across different IP addresses. - final suggestionUrl = Uri.parse( - "https://search.azlyrics.com/suggest.php?q=$title ${artists[0]}&x=884911ec808d4712b839f06754f62ef23cddd06a36e86bf8d44fbd2bac3e6a56"); - const Map headers = { HttpHeaders.userAgentHeader: - "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600" + "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600", }; - final searchResponse = await http.get(suggestionUrl, headers: headers); + //Will throw error 400 when you request the script with the host header + const Map headersForScript = { + HttpHeaders.userAgentHeader: + "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600", + HttpHeaders.hostHeader: "www.azlyrics.com", + }; + + final azLyricsGeoScript = await http.get( + Uri.parse("https://www.azlyrics.com/geo.js"), + headers: headersForScript); + + RegExp scriptValueRegex = RegExp(r'ep\.setAttribute\("value", "(.*)"\);'); + RegExp scriptNameRegex = RegExp(r'ep\.setAttribute\("name", "(.*)"\);'); + final String? v = + scriptValueRegex.firstMatch(azLyricsGeoScript.body)?.group(1); + final String? x = + scriptNameRegex.firstMatch(azLyricsGeoScript.body)?.group(1); + + debugPrint("getAZLyrics -> Additional URL params: $x=$v"); + + final suggestionUrl = Uri.parse( + "https://search.azlyrics.com/suggest.php?q=$title ${artists[0]}&${x.toString()}=${v.toString()}"); + + final searchResponse = await http.get(suggestionUrl, headers: headers); if (searchResponse.statusCode != 200) { throw "searchResponse = ${searchResponse.statusCode}"; } @@ -161,7 +180,7 @@ abstract class ServiceUtils { final String lyrics = lyricsDiv[4].text; - return lyrics; + return lyrics.trim(); } @Deprecated("In favor spotify lyrics api, this isn't needed anymore") From 5ebed57549926925620295fefede5ae2cfb7cf7f Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Fri, 23 Feb 2024 00:11:28 +0530 Subject: [PATCH 05/10] Cleanup title for better AZLyrics search result --- lib/utils/service_utils.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 84f38f6a5..1e69c0a87 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -123,10 +123,10 @@ abstract class ServiceUtils { {required String title, required List artists}) async { const Map headers = { HttpHeaders.userAgentHeader: - "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600", + "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600" }; - //Will throw error 400 when you request the script with the host header + //Will throw error 400 when you request the script without the host header const Map headersForScript = { HttpHeaders.userAgentHeader: "Mozilla/5.0 (Linux i656 ; en-US) AppleWebKit/601.49 (KHTML, like Gecko) Chrome/51.0.1145.334 Safari/600", @@ -137,8 +137,8 @@ abstract class ServiceUtils { Uri.parse("https://www.azlyrics.com/geo.js"), headers: headersForScript); - RegExp scriptValueRegex = RegExp(r'ep\.setAttribute\("value", "(.*)"\);'); - RegExp scriptNameRegex = RegExp(r'ep\.setAttribute\("name", "(.*)"\);'); + RegExp scriptValueRegex = RegExp(r'\.setAttribute\("value", "(.*)"\);'); + RegExp scriptNameRegex = RegExp(r'\.setAttribute\("name", "(.*)"\);'); final String? v = scriptValueRegex.firstMatch(azLyricsGeoScript.body)?.group(1); final String? x = @@ -147,7 +147,7 @@ abstract class ServiceUtils { debugPrint("getAZLyrics -> Additional URL params: $x=$v"); final suggestionUrl = Uri.parse( - "https://search.azlyrics.com/suggest.php?q=$title ${artists[0]}&${x.toString()}=${v.toString()}"); + "https://search.azlyrics.com/suggest.php?q=${title.replaceAll(RegExp(r"(\(.*\))"), "")} ${artists[0]}&${x.toString()}=${v.toString()}"); final searchResponse = await http.get(suggestionUrl, headers: headers); if (searchResponse.statusCode != 200) { From 07bc8f4665e05b6d76ceb7468670d4bd852fcf36 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Sat, 24 Feb 2024 22:02:20 +0530 Subject: [PATCH 06/10] fixed imports --- lib/utils/service_utils.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 1e69c0a87..04a58f9eb 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/widgets.dart' hide Element; import 'package:go_router/go_router.dart'; @@ -144,7 +145,7 @@ abstract class ServiceUtils { final String? x = scriptNameRegex.firstMatch(azLyricsGeoScript.body)?.group(1); - debugPrint("getAZLyrics -> Additional URL params: $x=$v"); + logger.t("Additional URL params: $x=$v"); final suggestionUrl = Uri.parse( "https://search.azlyrics.com/suggest.php?q=${title.replaceAll(RegExp(r"(\(.*\))"), "")} ${artists[0]}&${x.toString()}=${v.toString()}"); @@ -160,7 +161,7 @@ abstract class ServiceUtils { try { bestLyricsURL = searchResult["songs"][0]["url"]; - debugPrint("getAZLyrics -> bestLyricsURL: $bestLyricsURL"); + logger.t("bestLyricsURL: $bestLyricsURL"); } catch (e) { throw "No best Lyrics URL"; } From e3e7c22e620d1fdb445af3646dcaad940ca78f67 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Sun, 25 Feb 2024 01:50:09 +0530 Subject: [PATCH 07/10] Added method to fetch genius lyrics url This way, users don't require genius API access tokens. --- lib/utils/service_utils.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 04a58f9eb..43c2a7f9f 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -120,6 +120,34 @@ abstract class ServiceUtils { return results; } + static Future getGeniusLyrics( + {required String title, required List artists}) async { + //Requires a non-blacklisted, valid User Agent. Or else, cloudflare might throw a 403. + Map headers = { + HttpHeaders.userAgentHeader: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.4", + }; + + final searchResultResponse = await http.get( + Uri.parse( + "https://genius.com/api/search/multi?q=${title.replaceAll(RegExp(r"(\(.*\))"), "")} ${artists[0]}"), + headers: headers); + final searchResultObj = jsonDecode(searchResultResponse.body); + String topResultPath; + try { + topResultPath = searchResultObj["response"]["sections"][0]["hits"][0] + ["result"]["path"] as String; + logger.t("topResultPath: $topResultPath"); + } catch (e) { + logger.e(e); + throw "topResultPath not found!"; + } + final lyrics = + await extractLyrics(Uri.parse("https://genius.com$topResultPath")); + + return lyrics?.trim(); + } + static Future getAZLyrics( {required String title, required List artists}) async { const Map headers = { From 40dae56fd9ac55b728566040c3c8bd4c8d03d933 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Sun, 25 Feb 2024 01:53:49 +0530 Subject: [PATCH 08/10] Added query for genius lyrics --- lib/services/queries/lyrics.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index bc22fbe4e..971752a68 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -1,5 +1,4 @@ import 'dart:convert'; - import 'package:collection/collection.dart'; import 'package:fl_query/fl_query.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart'; @@ -28,6 +27,20 @@ class LyricsQueries { }); } + Query geniusLyrics(Track? track) { + return useQuery("geniusLyrics-query/${track?.id}", + () async { + if (track == null) { + throw "No Track Currently"; + } + final lyrics = await ServiceUtils.getGeniusLyrics( + title: track.name!, + artists: + track.artists?.map((s) => s.name).whereNotNull().toList() ?? []); + return lyrics; + }); + } + Query static( Track? track, From faa4af01a998e71099e990bbefe145a3bc5245f1 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Sun, 25 Feb 2024 02:02:08 +0530 Subject: [PATCH 09/10] Added fallback for AZLyrics Now, it will fetch lyrics from genius.com if they're not found on AZLyrics. --- lib/pages/lyrics/plain_lyrics.dart | 45 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 937b54ad8..d3545cc2e 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -33,11 +33,13 @@ class PlainLyrics extends HookConsumerWidget { final lyricsQuery = useQueries.lyrics.spotifySynced(ref, playlist.activeTrack); final azLyricsQuery = useQueries.lyrics.azLyrics(playlist.activeTrack); + final geniusLyricsQuery = + useQueries.lyrics.geniusLyrics(playlist.activeTrack); final mediaQuery = MediaQuery.of(context); final textTheme = Theme.of(context).textTheme; final textZoomLevel = useState(defaultTextZoom); - bool useAZLyrics = false; + bool useAZLyrics = false, useGenius = false; return Stack( children: [ @@ -78,9 +80,13 @@ class PlainLyrics extends HookConsumerWidget { return const ShimmerLyrics(); } else if (lyricsQuery.hasError) { if (azLyricsQuery.isLoading || - azLyricsQuery.isRefreshing) { + azLyricsQuery.isRefreshing || + geniusLyricsQuery.isLoading || + geniusLyricsQuery.isRefreshing) { return const ShimmerLyrics(); - } else if (azLyricsQuery.hasError) { + } + if (azLyricsQuery.hasError && + geniusLyricsQuery.hasError) { return Container( alignment: Alignment.center, padding: const EdgeInsets.all(16), @@ -99,24 +105,33 @@ class PlainLyrics extends HookConsumerWidget { ], ), ); + } else if (azLyricsQuery.hasError) { + useGenius = true; + } else if (geniusLyricsQuery.hasError) { + useAZLyrics = true; } else { useAZLyrics = true; } } - final lyrics = !useAZLyrics - ? lyricsQuery.data?.lyrics.mapIndexed((i, e) { - final next = lyricsQuery.data?.lyrics - .elementAtOrNull(i + 1); - if (next != null && - e.time - next.time > - const Duration(milliseconds: 700)) { - return "${e.text}\n"; - } + String? lyrics; + if (useAZLyrics) { + lyrics = azLyricsQuery.data; + } else if (useGenius) { + lyrics = geniusLyricsQuery.data; + } else { + lyrics = lyricsQuery.data?.lyrics.mapIndexed((i, e) { + final next = + lyricsQuery.data?.lyrics.elementAtOrNull(i + 1); + if (next != null && + e.time - next.time > + const Duration(milliseconds: 700)) { + return "${e.text}\n"; + } - return e.text; - }).join("\n") - : azLyricsQuery.data; + return e.text; + }).join("\n"); + } return AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), From 8ad12b5f42a40cccc5f591d35a8a3a972960c9c0 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Das Date: Sun, 25 Feb 2024 15:18:41 +0530 Subject: [PATCH 10/10] Removed unnecessary logging --- lib/utils/service_utils.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 43c2a7f9f..67b51c1f0 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -137,9 +137,9 @@ abstract class ServiceUtils { try { topResultPath = searchResultObj["response"]["sections"][0]["hits"][0] ["result"]["path"] as String; - logger.t("topResultPath: $topResultPath"); + logger.t("topResultUrl: https://genius.com$topResultPath"); } catch (e) { - logger.e(e); + //logger.e(e); throw "topResultPath not found!"; } final lyrics =