diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 32484a1b..79890dce 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart' hide Image; import 'package:spotube/components/CategoryCard.dart'; import 'package:spotube/components/Login.dart'; +import 'package:spotube/components/Lyrics.dart'; import 'package:spotube/components/PageWindowTitleBar.dart'; import 'package:spotube/components/Player.dart' as player; import 'package:spotube/components/Settings.dart'; @@ -239,7 +240,7 @@ class _HomeState extends State { ), ), if (_selectedIndex == 2) const UserLibrary(), - // player itself + if (_selectedIndex == 3) const Lyrics(), ], ), ), diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart new file mode 100644 index 00000000..220ea347 --- /dev/null +++ b/lib/components/Lyrics.dart @@ -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 createState() => _LyricsState(); +} + +class _LyricsState extends State { + Map? _lyrics; + + @override + Widget build(BuildContext context) { + Playback playback = context.watch(); + + 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"), + ), + ) + ], + ), + ); + } +} diff --git a/lib/components/Player.dart b/lib/components/Player.dart index 668bad65..1239999a 100644 --- a/lib/components/Player.dart +++ b/lib/components/Player.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:spotube/components/PlayerControls.dart'; +import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:mpv_dart/mpv_dart.dart'; @@ -128,10 +129,6 @@ class _PlayerState extends State { } } - String artistsToString(List artists) { - return artists.map((e) => e.name?.replaceAll(",", " ")).join(", "); - } - @override Widget build(BuildContext context) { MPVPlayer player = context.watch().player; diff --git a/lib/helpers/artist-to-string.dart b/lib/helpers/artist-to-string.dart new file mode 100644 index 00000000..36d09b25 --- /dev/null +++ b/lib/helpers/artist-to-string.dart @@ -0,0 +1,5 @@ +import 'package:spotify/spotify.dart'; + +String artistsToString(List artists) { + return artists.map((e) => e.name?.replaceAll(",", " ")).join(", "); +} diff --git a/lib/helpers/getLyrics.dart b/lib/helpers/getLyrics.dart new file mode 100644 index 00000000..b7034f8d --- /dev/null +++ b/lib/helpers/getLyrics.dart @@ -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 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("
", "\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 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 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 { + "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 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); + } +} diff --git a/lib/models/sideBarTiles.dart b/lib/models/sideBarTiles.dart index c4275788..9de32755 100644 --- a/lib/models/sideBarTiles.dart +++ b/lib/models/sideBarTiles.dart @@ -10,4 +10,5 @@ List sidebarTileList = [ SideBarTiles(icon: Icons.home_rounded, title: "Browse"), SideBarTiles(icon: Icons.search_rounded, title: "Search"), SideBarTiles(icon: Icons.library_books_rounded, title: "Library"), + SideBarTiles(icon: Icons.music_note_rounded, title: "Lyrics") ];