mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Lyrics support added
This commit is contained in:
parent
6ea222c5b0
commit
50b835e122
@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:spotify/spotify.dart' hide Image;
|
import 'package:spotify/spotify.dart' hide Image;
|
||||||
import 'package:spotube/components/CategoryCard.dart';
|
import 'package:spotube/components/CategoryCard.dart';
|
||||||
import 'package:spotube/components/Login.dart';
|
import 'package:spotube/components/Login.dart';
|
||||||
|
import 'package:spotube/components/Lyrics.dart';
|
||||||
import 'package:spotube/components/PageWindowTitleBar.dart';
|
import 'package:spotube/components/PageWindowTitleBar.dart';
|
||||||
import 'package:spotube/components/Player.dart' as player;
|
import 'package:spotube/components/Player.dart' as player;
|
||||||
import 'package:spotube/components/Settings.dart';
|
import 'package:spotube/components/Settings.dart';
|
||||||
@ -239,7 +240,7 @@ class _HomeState extends State<Home> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_selectedIndex == 2) const UserLibrary(),
|
if (_selectedIndex == 2) const UserLibrary(),
|
||||||
// player itself
|
if (_selectedIndex == 3) const Lyrics(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
84
lib/components/Lyrics.dart
Normal file
84
lib/components/Lyrics.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
|
import 'package:spotube/helpers/getLyrics.dart';
|
||||||
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
|
||||||
|
class Lyrics extends StatefulWidget {
|
||||||
|
const Lyrics({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Lyrics> createState() => _LyricsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LyricsState extends State<Lyrics> {
|
||||||
|
Map<String, String>? _lyrics;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Playback playback = context.watch<Playback>();
|
||||||
|
|
||||||
|
if (playback.currentTrack != null &&
|
||||||
|
playback.currentTrack!.id != _lyrics?["id"]) {
|
||||||
|
getLyrics(
|
||||||
|
playback.currentTrack!.name!,
|
||||||
|
artistsToString(playback.currentTrack!.artists ?? []),
|
||||||
|
apiKey:
|
||||||
|
"O6K9JcMNsVD36lRJM6wvl0YsfjrtHFFfAwYHZqxxTNg2xBuMxcaJXrYbpR6kVipN",
|
||||||
|
optimizeQuery: true,
|
||||||
|
).then((lyrics) {
|
||||||
|
if (lyrics != null) {
|
||||||
|
setState(() {
|
||||||
|
_lyrics = {"lyrics": lyrics, "id": playback.currentTrack!.id!};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lyrics == null && playback.currentTrack != null) {
|
||||||
|
return const Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
playback.currentTrack!.name!,
|
||||||
|
style: Theme.of(context).textTheme.headline3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
artistsToString(playback.currentTrack!.artists ?? []),
|
||||||
|
style: Theme.of(context).textTheme.headline5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
_lyrics == null && playback.currentTrack == null
|
||||||
|
? "No Track being played currently"
|
||||||
|
: _lyrics!["lyrics"]!,
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Text("Powered by genius.com"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:spotube/components/PlayerControls.dart';
|
import 'package:spotube/components/PlayerControls.dart';
|
||||||
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mpv_dart/mpv_dart.dart';
|
import 'package:mpv_dart/mpv_dart.dart';
|
||||||
@ -128,10 +129,6 @@ class _PlayerState extends State<Player> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String artistsToString(List<Artist> artists) {
|
|
||||||
return artists.map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
MPVPlayer player = context.watch<PlayerDI>().player;
|
MPVPlayer player = context.watch<PlayerDI>().player;
|
||||||
|
5
lib/helpers/artist-to-string.dart
Normal file
5
lib/helpers/artist-to-string.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
String artistsToString(List<Artist> artists) {
|
||||||
|
return artists.map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||||
|
}
|
104
lib/helpers/getLyrics.dart
Normal file
104
lib/helpers/getLyrics.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:html/parser.dart' as parser;
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
String getTitle(String title, String artist) {
|
||||||
|
return "$title $artist"
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(" *\\([^)]*\\) *"), '')
|
||||||
|
.replaceAll(RegExp(" *\\[[^\\]]*]"), '')
|
||||||
|
.replaceAll(RegExp("feat.|ft."), '')
|
||||||
|
.replaceAll(RegExp("\\s+"), ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> extractLyrics(Uri url) async {
|
||||||
|
try {
|
||||||
|
var response = await http.get(url);
|
||||||
|
|
||||||
|
Document document = parser.parse(response.body);
|
||||||
|
var lyrics = document.querySelector('div.lyrics')?.text.trim();
|
||||||
|
if (lyrics == null) {
|
||||||
|
lyrics = "";
|
||||||
|
document
|
||||||
|
.querySelectorAll("div[class^=\"Lyrics__Container\"]")
|
||||||
|
.forEach((element) {
|
||||||
|
if (element.text.trim().isNotEmpty) {
|
||||||
|
var snippet = element.innerHtml.replaceAll("<br>", "\n").replaceAll(
|
||||||
|
RegExp("<(?!\\s*br\\s*\\/?)[^>]+>", caseSensitive: false),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
var el = document.createElement("textarea");
|
||||||
|
el.innerHtml = snippet;
|
||||||
|
lyrics = "$lyrics${el.text.trim()}\n\n";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lyrics;
|
||||||
|
} catch (e, stack) {
|
||||||
|
print("[extractLyrics] $e");
|
||||||
|
print(stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List?> searchSong(
|
||||||
|
String title,
|
||||||
|
String artist, {
|
||||||
|
String apiKey = "",
|
||||||
|
bool optimizeQuery = false,
|
||||||
|
bool authHeader = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
const searchUrl = 'https://api.genius.com/search?q=';
|
||||||
|
String song = optimizeQuery ? getTitle(title, artist) : "$title $artist";
|
||||||
|
|
||||||
|
String reqUrl = "$searchUrl${Uri.encodeComponent(song)}";
|
||||||
|
Map<String, String> headers = {"Authorization": 'Bearer ' + apiKey};
|
||||||
|
var response = await http.get(
|
||||||
|
Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"),
|
||||||
|
headers: authHeader ? headers : null,
|
||||||
|
);
|
||||||
|
Map data = jsonDecode(response.body)["response"];
|
||||||
|
if (data["hits"]?.length == 0) return null;
|
||||||
|
List results = data["hits"]?.map((val) {
|
||||||
|
return <String, dynamic>{
|
||||||
|
"id": val["result"]["id"],
|
||||||
|
"full_title": val["result"]["full_title"],
|
||||||
|
"albumArt": val["result"]["song_art_image_url"],
|
||||||
|
"url": val["result"]["url"]
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
return results;
|
||||||
|
} catch (e, stack) {
|
||||||
|
print("[searchSong] $e");
|
||||||
|
print(stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLyrics(
|
||||||
|
String title,
|
||||||
|
String artist, {
|
||||||
|
String apiKey = "",
|
||||||
|
bool optimizeQuery = false,
|
||||||
|
bool authHeader = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
var results = await searchSong(
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
apiKey: apiKey,
|
||||||
|
optimizeQuery: optimizeQuery,
|
||||||
|
authHeader: authHeader,
|
||||||
|
);
|
||||||
|
if (results == null) return null;
|
||||||
|
String? lyrics = await extractLyrics(Uri.parse(results.first["url"]));
|
||||||
|
return lyrics;
|
||||||
|
} catch (e, stack) {
|
||||||
|
print("[getLyrics] $e");
|
||||||
|
print(stack);
|
||||||
|
}
|
||||||
|
}
|
@ -10,4 +10,5 @@ List<SideBarTiles> sidebarTileList = [
|
|||||||
SideBarTiles(icon: Icons.home_rounded, title: "Browse"),
|
SideBarTiles(icon: Icons.home_rounded, title: "Browse"),
|
||||||
SideBarTiles(icon: Icons.search_rounded, title: "Search"),
|
SideBarTiles(icon: Icons.search_rounded, title: "Search"),
|
||||||
SideBarTiles(icon: Icons.library_books_rounded, title: "Library"),
|
SideBarTiles(icon: Icons.library_books_rounded, title: "Library"),
|
||||||
|
SideBarTiles(icon: Icons.music_note_rounded, title: "Lyrics")
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user