Skip to content

Commit

Permalink
feat: lyrics for youtube
Browse files Browse the repository at this point in the history
(only when youtube-style miniplayer disabled)
  • Loading branch information
MSOB7YY committed Jun 9, 2024
1 parent 6cd441d commit 31af954
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 177 deletions.
2 changes: 2 additions & 0 deletions lib/base/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
canPlayAudioOnlyFromCache ??= (_isAudioOnlyPlayback || !ConnectivityController.inst.hasConnection);

WaveformController.inst.resetWaveform();
Lyrics.inst.resetLyrics();

YoutubeController.inst.currentYTQualities.clear();
YoutubeController.inst.currentYTAudioStreams.clear();
Expand Down Expand Up @@ -820,6 +821,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
startSleepAfterMinCount();
startCounterToAListen(pi);
increaseListenTime(LibraryCategory.youtube);
Lyrics.inst.updateLyrics(item);
}
}

Expand Down
180 changes: 64 additions & 116 deletions lib/controller/lyrics_controller.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
/// copyright: google search request is originally from [@netlob](https://github.com/netlob/dart-lyrics), edited to fit Namida.
library;

// ignore_for_file: depend_on_referenced_packages

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:namida/core/utils.dart';
import 'package:http/http.dart' as http;
import 'package:lrc/lrc.dart';
import 'package:namida/youtube/class/youtube_id.dart';
import 'package:path/path.dart' as p;

import 'package:namida/base/ports_provider.dart';
import 'package:namida/class/http_response_wrapper.dart';
import 'package:namida/class/lyrics.dart';
import 'package:namida/class/track.dart';
import 'package:namida/controller/lyrics_search_utils/lrc_search_details.dart';
import 'package:namida/controller/lyrics_search_utils/lrc_search_utils_base.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/controller/wakelock_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/enums.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/packages/lyrics_lrc_parsed_view.dart';
import 'package:namida/youtube/class/youtube_id.dart';

class Lyrics {
static Lyrics get inst => _instance;
Expand All @@ -43,6 +40,7 @@ class Lyrics {
Playable? _currentItem;

bool get _lyricsEnabled => settings.enableLyrics.value;
bool get _canDisplayLRCForYoutubeID => settings.youtubeStyleMiniplayer.value == false;
bool get _lyricsPrioritizeEmbedded => settings.prioritizeEmbeddedLyrics.value;
LyricsSource get _lyricsSource => settings.lyricsSource.value;

Expand Down Expand Up @@ -73,120 +71,84 @@ class Lyrics {

lyricsCanBeAvailable.value = true;
if (!_lyricsEnabled) return;
if (item is YoutubeID && !_canDisplayLRCForYoutubeID) return;

if (item is YoutubeID) {
// TODO: allow lyrics for youtube videos
} else if (item is Selectable) {
final track = item.track;
final LrcSearchUtils? lrcUtils = LrcSearchUtils.fromPlayable(item);

final embedded = track.lyrics;
if (lrcUtils == null) return;

if (_lyricsPrioritizeEmbedded && embedded != '') {
final lrc = embedded.parseLRC();
if (lrc != null && lrc.lyrics.isNotEmpty) {
currentLyricsLRC.value = lrc;
_updateWidgets(lrc);
} else {
currentLyricsText.value = embedded;
}
return;
final embedded = lrcUtils.embeddedLyrics;
if (_lyricsPrioritizeEmbedded && embedded != '') {
final lrc = embedded.parseLRC();
if (lrc != null && lrc.lyrics.isNotEmpty) {
currentLyricsLRC.value = lrc;
_updateWidgets(lrc);
} else {
currentLyricsText.value = embedded;
}
return;
}

/// 1. device lrc
/// 2. cached lrc
/// 3. track embedded lrc
/// 4. database.
final lrcLyrics = await _fetchLRCBasedLyrics(track, embedded, _lyricsSource);

if (checkInterrupted()) return;

if (lrcLyrics.$1 != null) {
currentLyricsLRC.value = lrcLyrics.$1;
_updateWidgets(lrcLyrics.$1);
return;
} else if (lrcLyrics.$2 != null) {
currentLyricsText.value = lrcLyrics.$2 ?? '';
_updateWidgets(null);
return;
}
/// 1. device lrc
/// 2. cached lrc
/// 3. track embedded lrc
/// 4. database.
final lrcLyrics = await _fetchLRCBasedLyrics(lrcUtils, embedded, _lyricsSource);

if (checkInterrupted()) return;

if (lrcLyrics.$1 != null) {
currentLyricsLRC.value = lrcLyrics.$1;
_updateWidgets(lrcLyrics.$1);
return;
} else if (lrcLyrics.$2 != null) {
currentLyricsText.value = lrcLyrics.$2 ?? '';
_updateWidgets(null);
return;
}

if (checkInterrupted()) return;
if (checkInterrupted()) return;

/// 1. cached txt lyrics
/// 2. track embedded txt
/// 3. google search
final textLyrics = await _fetchTextBasedLyrics(track, embedded, _lyricsSource);
/// 1. cached txt lyrics
/// 2. track embedded txt
/// 3. google search
final textLyrics = await _fetchTextBasedLyrics(lrcUtils, embedded, _lyricsSource);

if (checkInterrupted()) return;
if (checkInterrupted()) return;

if (textLyrics != '') {
currentLyricsText.value = textLyrics;
} else {
lyricsCanBeAvailable.value = false;
}
if (textLyrics != '') {
currentLyricsText.value = textLyrics;
} else {
lyricsCanBeAvailable.value = false;
}
}

bool hasLyrics(Track tr) {
return tr.lyrics != '' || lyricsFileCacheLRC(tr).existsSync() || lyricsFilesDevice(tr).any((element) => element.existsSync()) || lyricsFileCacheText(tr).existsSync();
}

File lyricsFileCacheText(Track tr) => File(p.join(AppDirs.LYRICS, "${tr.filename}.txt"));
File lyricsFileCacheLRC(Track tr) => File(p.join(AppDirs.LYRICS, "${tr.filename}.lrc"));
List<File> lyricsFilesDevice(Track tr) {
final dirPath = tr.path.getDirectoryPath;
return [
File(p.join(dirPath, "${tr.filename}.lrc")),
File(p.join(dirPath, "${tr.filenameWOExt}.lrc")),
File(p.join(dirPath, "${tr.filename}.LRC")),
File(p.join(dirPath, "${tr.filenameWOExt}.LRC")),
];
}

Future<void> saveLyricsToCache(Track track, String lyricsText, bool isSynced) async {
final fc = isSynced ? lyricsFileCacheLRC(track) : lyricsFileCacheText(track);
await fc.create();
await fc.writeAsString(lyricsText);
}

Future<List<LyricsModel>> searchLRCLyricsFromInternet({
required TrackExtended? trackExt,
String customQuery = '',
}) async {
if (trackExt == null && customQuery == '') return [];
var searchTries = <_LRCSearchDetails>[];
if (trackExt != null) {
final durS = trackExt.duration;
searchTries = [
_LRCSearchDetails(title: trackExt.title, artist: trackExt.originalArtist, album: '', durationSeconds: durS),
_LRCSearchDetails(title: trackExt.title, artist: trackExt.originalArtist, album: trackExt.album, durationSeconds: durS),
if (trackExt.artistsList.isNotEmpty) _LRCSearchDetails(title: trackExt.title, artist: trackExt.artistsList.first, album: '', durationSeconds: durS),
if (trackExt.artistsList.isNotEmpty) _LRCSearchDetails(title: trackExt.title, artist: trackExt.artistsList.first, album: trackExt.album, durationSeconds: durS),
];
}
Future<List<LyricsModel>> searchLRCLyricsFromInternet({required LrcSearchUtils lrcUtils, String? customQuery}) async {
final searchTries = lrcUtils.searchDetailsQueries();
if (searchTries.isEmpty && (customQuery == null || customQuery == '')) return [];

return await _lrcSearchManager.search(
queries: searchTries,
customQuery: customQuery,
);
}

Future<(Lrc?, String?)> _fetchLRCBasedLyrics(Track track, String trackLyrics, LyricsSource source) async {
Future<(Lrc?, String?)> _fetchLRCBasedLyrics(LrcSearchUtils lrcUtils, String trackLyrics, LyricsSource source) async {
String? lrcContent;

/// 1. device lrc
/// 2. cached lrc
/// 3. track embedded
if (source != LyricsSource.internet) {
final lyricsFilesLocal = lyricsFilesDevice(track);
final lyricsFilesLocal = lrcUtils.deviceLRCFiles;
for (final lf in lyricsFilesLocal) {
if (await lf.existsAndValid()) {
lrcContent = await lf.readAsString();
break;
}
}
if (lrcContent == null) {
final syncedInCache = lyricsFileCacheLRC(track);
final syncedInCache = lrcUtils.cachedLRCFile;
if (await syncedInCache.existsAndValid()) {
lrcContent = await syncedInCache.readAsString();
} else if (trackLyrics != '') {
Expand All @@ -197,17 +159,16 @@ class Lyrics {

/// 4. if still null, fetch from database.
if (source != LyricsSource.local && lrcContent == null) {
final trackExt = track.toTrackExt();
final lyrics = await searchLRCLyricsFromInternet(trackExt: trackExt);
final lyrics = await searchLRCLyricsFromInternet(lrcUtils: lrcUtils);
final lyricsModelToUse = lyrics.firstOrNull;
if (lyricsModelToUse != null && lyricsModelToUse.lyrics.isNotEmpty == true) {
final parsedLrc = lyricsModelToUse.synced ? lyricsModelToUse.lyrics.parseLRC() : null;
if (parsedLrc != null) {
final syncedInCache = lyricsFileCacheLRC(track);
final syncedInCache = lrcUtils.cachedLRCFile;
await syncedInCache.writeAsString(lyricsModelToUse.lyrics);
return (parsedLrc, null);
} else {
final plainInCache = lyricsFileCacheText(track);
final plainInCache = lrcUtils.cachedTxtFile;
await plainInCache.writeAsString(lyricsModelToUse.lyrics);
return (null, lyricsModelToUse.lyrics);
}
Expand All @@ -222,8 +183,8 @@ class Lyrics {
}
}

Future<String> _fetchTextBasedLyrics(Track track, String trackLyrics, LyricsSource source) async {
final lyricsFile = lyricsFileCacheText(track);
Future<String> _fetchTextBasedLyrics(LrcSearchUtils lrcUtils, String trackLyrics, LyricsSource source) async {
final lyricsFile = lrcUtils.cachedTxtFile;

/// get from storage
if (source != LyricsSource.internet && await lyricsFile.existsAndValid()) {
Expand All @@ -234,7 +195,7 @@ class Lyrics {

/// download lyrics
else if (source != LyricsSource.local) {
final lyrics = await _fetchLyricsGoogle(artist: track.artistsList.firstOrNull ?? '', title: track.title);
final lyrics = await _fetchLyricsGoogle(lrcUtils.searchQueriesGoogle());
final regex = RegExp(r'<[^>]*>');
if (lyrics != '') {
final formattedText = lyrics.replaceAll(regex, '');
Expand All @@ -245,15 +206,8 @@ class Lyrics {
return '';
}

Future<String> _fetchLyricsGoogle({String title = '', String artist = ''}) async {
if (title == '' && artist == '') return '';

final possibleQueries = <String>[
'$title by $artist lyrics',
'${title.split("-").first} by $artist lyrics',
'$title by $artist song lyrics',
];

Future<String> _fetchLyricsGoogle(List<String> possibleQueries) async {
if (possibleQueries.isEmpty) return '';
return await _fetchLyricsGoogleIsolate.thready(possibleQueries);
}

Expand Down Expand Up @@ -294,27 +248,21 @@ class Lyrics {
}
}

class _LRCSearchDetails {
final String title, artist, album;
final int durationSeconds;
const _LRCSearchDetails({required this.title, required this.artist, required this.album, required this.durationSeconds});
}

class _LRCSearchManager with PortsProvider<SendPort> {
_LRCSearchManager();

Completer<List<LyricsModel>>? _completer;

/// if [file] is temp, u can provide [moveTo] to move/rename the temp file to it.
Future<List<LyricsModel>> search({
required List<_LRCSearchDetails> queries,
String customQuery = '',
required List<LRCSearchDetails> queries,
String? customQuery,
}) async {
_completer?.completeIfWasnt([]);
_completer = Completer<List<LyricsModel>>();

await initialize();
final p = customQuery != '' ? customQuery : queries;
final p = customQuery != null && customQuery.isNotEmpty ? customQuery : queries;
await sendPort(p);
final res = await _completer?.future ?? [];
_completer = null;
Expand Down Expand Up @@ -346,7 +294,7 @@ class _LRCSearchManager with PortsProvider<SendPort> {
}

Future<List<LyricsModel>> fetchLRCBasedLyricsFromInternet({
_LRCSearchDetails? details,
LRCSearchDetails? details,
String customQuery = '',
required HttpClientWrapper requester,
}) async {
Expand Down Expand Up @@ -441,7 +389,7 @@ class _LRCSearchManager with PortsProvider<SendPort> {
final c = mainRequester!; // instance so it can be closed

var lyrics = <LyricsModel>[];
if (p is List<_LRCSearchDetails>) {
if (p is List<LRCSearchDetails>) {
for (final details in p) {
lyrics = await fetchLRCBasedLyricsFromInternet(
details: details,
Expand Down
11 changes: 11 additions & 0 deletions lib/controller/lyrics_search_utils/lrc_search_details.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class LRCSearchDetails {
final String title, artist, album;
final int durationSeconds;

const LRCSearchDetails({
required this.title,
required this.artist,
required this.album,
required this.durationSeconds,
});
}
46 changes: 46 additions & 0 deletions lib/controller/lyrics_search_utils/lrc_search_utils_base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'dart:io';

import 'package:flutter/foundation.dart';

import 'package:namida/class/track.dart';
import 'package:namida/controller/lyrics_search_utils/lrc_search_details.dart';
import 'package:namida/controller/lyrics_search_utils/lrc_search_utils_selectable.dart';
import 'package:namida/controller/lyrics_search_utils/lrc_search_utils_youtubeid.dart';
import 'package:namida/youtube/class/youtube_id.dart';
import 'package:namida/youtube/controller/youtube_controller.dart';

abstract class LrcSearchUtils {
const LrcSearchUtils();

static LrcSearchUtils? fromPlayable(Playable item) {
if (item is Selectable) {
final tr = item.track;
return LrcSearchUtilsSelectable(tr.toTrackExt(), tr);
} else if (item is YoutubeID) {
final videoInfo = YoutubeController.inst.getVideoInfo(item.id, checkFromStorage: true);
return LrcSearchUtilsYoutubeID(item, videoInfo?.name);
}
return null;
}

String get initialSearchTextHint;
String? get pickFileInitialDirectory;
String get embeddedLyrics;
File get cachedTxtFile;
File get cachedLRCFile;
List<File> get deviceLRCFiles;

Future<void> saveLyricsToCache(String formatted, bool isSynced) async {
final fc = isSynced ? cachedLRCFile : cachedTxtFile;
await fc.create();
await fc.writeAsString(formatted);
}

@mustCallSuper
bool hasLyrics() {
return cachedLRCFile.existsSync() || deviceLRCFiles.any((element) => element.existsSync()) || cachedTxtFile.existsSync();
}

List<LRCSearchDetails> searchDetailsQueries();
List<String> searchQueriesGoogle();
}
Loading

0 comments on commit 31af954

Please sign in to comment.