Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added method to fetch lyrics from AZLyrics or Genius as a fallback #1251

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
81 changes: 53 additions & 28 deletions lib/pages/lyrics/plain_lyrics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ 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 geniusLyricsQuery =
useQueries.lyrics.geniusLyrics(playlist.activeTrack);
final mediaQuery = MediaQuery.of(context);
final textTheme = Theme.of(context).textTheme;

final textZoomLevel = useState<int>(defaultTextZoom);
bool useAZLyrics = false, useGenius = false;

return Stack(
children: [
Expand Down Expand Up @@ -75,38 +79,59 @@ 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 ||
geniusLyricsQuery.isLoading ||
geniusLyricsQuery.isRefreshing) {
return const ShimmerLyrics();
}
if (azLyricsQuery.hasError &&
geniusLyricsQuery.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 if (azLyricsQuery.hasError) {
useGenius = true;
} else if (geniusLyricsQuery.hasError) {
useAZLyrics = true;
} 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";
}
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");
return e.text;
}).join("\n");
}

return AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
Expand Down
29 changes: 28 additions & 1 deletion lib/services/queries/lyrics.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,6 +14,34 @@ import 'package:http/http.dart' as http;
class LyricsQueries {
const LyricsQueries();

Query<String, dynamic> azLyrics(Track? track) {
return useQuery<String, dynamic>("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<String, dynamic> geniusLyrics(Track? track) {
return useQuery<String, dynamic>("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<String, dynamic> static(
Track? track,
String geniusAccessToken,
Expand Down
93 changes: 93 additions & 0 deletions lib/utils/service_utils.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/widgets.dart' hide Element;
import 'package:go_router/go_router.dart';
Expand Down Expand Up @@ -119,6 +120,98 @@ abstract class ServiceUtils {
return results;
}

static Future<String?> getGeniusLyrics(
{required String title, required List<String> artists}) async {
//Requires a non-blacklisted, valid User Agent. Or else, cloudflare might throw a 403.
Map<String, String> 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("topResultUrl: https://genius.com$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<String?> getAZLyrics(
{required String title, required List<String> artists}) async {
const Map<String, String> headers = {
HttpHeaders.userAgentHeader:
"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 without the host header
const Map<String, String> 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",
Spectre-hidN marked this conversation as resolved.
Show resolved Hide resolved
};

final azLyricsGeoScript = await http.get(
Uri.parse("https://www.azlyrics.com/geo.js"),
headers: headersForScript);

RegExp scriptValueRegex = RegExp(r'\.setAttribute\("value", "(.*)"\);');
RegExp scriptNameRegex = RegExp(r'\.setAttribute\("name", "(.*)"\);');
final String? v =
scriptValueRegex.firstMatch(azLyricsGeoScript.body)?.group(1);
final String? x =
scriptNameRegex.firstMatch(azLyricsGeoScript.body)?.group(1);

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()}");

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"];
logger.t("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.trim();
}

@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static Future<String?> getLyrics(
String title,
Expand Down