Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ This project loosely follows Keep a Changelog and uses Semantic Versioning.

### Added

- find button discord RPC now links to YouTube Music search
- Find button discord RPC now links to YouTube Music search
- Added irclib fallback for lyrics
- [Android] Storage permissions handler
- [Android] URI path resolution
- [Android] Folder manager access on mobile
Expand Down
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void main() async {
settings: settings,
);
} catch (e, st) {
print('[aqloss] main init error: $e\n$st');
Logger.errorFrontend('[aqloss] main init error: $e\n$st');
}
});
}
Expand Down
85 changes: 82 additions & 3 deletions lib/providers/lyrics_provider.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:aqloss/util/logger.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:http/http.dart' as http;
import 'package:aqloss/providers/player_provider.dart';
import 'package:aqloss/src/rust/api.dart' as backend;

Expand Down Expand Up @@ -77,13 +80,18 @@ class LyricsState {
bool get hasSynced => document != null;
}

enum LyricsSource { none, embedded, lrcFile, txtFile }
enum LyricsSource { none, embedded, lrcFile, txtFile, lrclib }

// Notifier
class LyricsNotifier extends StateNotifier<LyricsState> {
LyricsNotifier() : super(const LyricsState());

Future<void> loadForTrack(String trackPath) async {
Future<void> loadForTrack(
String trackPath, {
String? artist,
String? title,
int? duration,
}) async {
if (state.trackPath == trackPath) return;
state = LyricsState(isLoading: true, trackPath: trackPath);

Expand Down Expand Up @@ -152,6 +160,24 @@ class LyricsNotifier extends StateNotifier<LyricsState> {
}
}

// lrclib fallback
if (artist != null && title != null) {
final lrclibResult = await _fetchFromLrclib(
artist: artist,
title: title,
duration: duration,
);
if (lrclibResult != null) {
state = LyricsState(
document: lrclibResult.document,
rawText: lrclibResult.rawText,
trackPath: trackPath,
source: LyricsSource.lrclib,
);
return;
}
}

state = LyricsState(trackPath: trackPath);
} catch (_) {
state = LyricsState(trackPath: trackPath);
Expand All @@ -163,14 +189,67 @@ class LyricsNotifier extends StateNotifier<LyricsState> {
}
}

// lrclib result container
class _LrclibResult {
final LrcDocument? document;
final String? rawText;
const _LrclibResult({this.document, this.rawText});
}

Future<_LrclibResult?> _fetchFromLrclib({
required String artist,
required String title,
int? duration,
}) async {
try {
final uri = Uri.https('lrclib.net', '/api/get', {
'artist_name': artist,
'track_name': title,
if (duration != null) 'duration': duration.toString(),
});
Logger.infoFrontend("Searching for $title lyrics: $uri");

final res = await http
.get(
uri,
headers: {
'User-Agent': 'aqloss/1.0 (https://nokarin.xyz/projects/aqloss)',
},
)
.timeout(const Duration(seconds: 8));

if (res.statusCode != 200) return null;

final json = jsonDecode(res.body) as Map<String, dynamic>;

// Prefer synced LRC over plain text
final syncedLyrics = json['syncedLyrics'] as String?;
if (syncedLyrics != null && syncedLyrics.trim().isNotEmpty) {
final doc = LrcDocument.parse(syncedLyrics);
if (doc != null) return _LrclibResult(document: doc);
}

final plainLyrics = json['plainLyrics'] as String?;
if (plainLyrics != null && plainLyrics.trim().isNotEmpty) {
return _LrclibResult(rawText: plainLyrics.trim());
}
} catch (_) {}
return null;
}

final lyricsProvider = StateNotifierProvider<LyricsNotifier, LyricsState>((
ref,
) {
final notifier = LyricsNotifier();
ref.listen<PlayerState>(playerProvider, (prev, next) {
final path = next.currentTrack?.path;
if (path != null && path != prev?.currentTrack?.path) {
notifier.loadForTrack(path);
notifier.loadForTrack(
path,
artist: next.currentTrack?.artist,
title: next.currentTrack?.title,
duration: next.currentTrack?.duration.inSeconds,
);
} else if (path == null) {
notifier.clear();
}
Expand Down
7 changes: 4 additions & 3 deletions lib/screens/player_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@ class _WideLayout extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final hasLyrics = ref.watch(lyricsProvider).hasLyrics;
ref.watch(lyricsProvider);
final settings = ref.watch(settingsProvider);
final cs = Theme.of(context).colorScheme;
final width = MediaQuery.of(context).size.width;
final showLyricsPanel = track != null;

return Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 320),
curve: Curves.easeInOutCubic,
width: hasLyrics ? width * 0.28 : width * 0.44,
width: showLyricsPanel ? width * 0.28 : width * 0.44,
child: Padding(
padding: const EdgeInsets.fromLTRB(28, 32, 16, 24),
child: Column(
Expand All @@ -58,7 +59,7 @@ class _WideLayout extends ConsumerWidget {
showBackground: settings.showAlbumArtBackground,
),
),
if (hasLyrics) ...[
if (showLyricsPanel) ...[
const SizedBox(height: 14),
Expanded(
child: ClipRRect(
Expand Down
10 changes: 4 additions & 6 deletions lib/services/audio_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,28 @@ class AudioService {
await Future.delayed(Duration(milliseconds: delays[attempt]));
}
try {
print('[aqloss] initEngine attempt ${attempt + 1} start');
Logger.debugAudioService('engine attempt ${attempt + 1} start');
if (deviceId != null) {
await backend
.initEngineWithDevice(deviceId: deviceId, exclusive: exclusive)
.timeout(const Duration(seconds: 8));
} else {
await backend.initEngine().timeout(const Duration(seconds: 8));
}
print('[aqloss] initEngine attempt ${attempt + 1} SUCCESS');
Logger.debugAudioService('engine attempt ${attempt + 1} SUCCESS');
_engineReady = true;
Logger.debugAudioService('engine ready (attempt ${attempt + 1})');
break;
} catch (e, st) {
print('[aqloss] initEngine attempt ${attempt + 1} FAILED: $e');
Logger.errorAudioService('init attempt ${attempt + 1} FAILED: $e\n$st');
if (attempt == delays.length - 1) {
try {
print('[aqloss] initEngine fallback start');
Logger.debugAudioService('initEngine fallback start');
await backend.initEngine().timeout(const Duration(seconds: 6));
print('[aqloss] initEngine fallback SUCCESS');
Logger.debugAudioService('[aqloss] initEngine fallback SUCCESS');
_engineReady = true;
Logger.debugAudioService('engine ready (fallback shared)');
} catch (e2, st2) {
print('[aqloss] initEngine FATAL: $e2');
Logger.errorAudioService('engine init FATAL: $e2\n$st2');
return;
}
Expand Down
19 changes: 13 additions & 6 deletions lib/util/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ enum LogTarget {
audioService('audio_service.log'),
lastfm('lastfm.log'),
deviceProdiver('device_provider.log'),
playerProvider('player_provider.log');
playerProvider('player_provider.log'),
frontend('frontend.log');

final String fileName;
const LogTarget(this.fileName);
Expand Down Expand Up @@ -56,11 +57,7 @@ class Logger {
──────────────────────────────────────────────────────\n""";

for (var file in _files.values) {
await file.writeAsString(
startSession,
mode: FileMode.write,
flush: true,
);
await file.writeAsString(startSession, mode: FileMode.write, flush: true);
}

_isInitialized = true;
Expand Down Expand Up @@ -136,4 +133,14 @@ class Logger {
_instance._log(LogTarget.playerProvider, LogLevel.warn, msg);
static void errorPlayerProvider(String msg) =>
_instance._log(LogTarget.playerProvider, LogLevel.error, msg);

// Frontend
static void debugFrontend(String msg) =>
_instance._log(LogTarget.frontend, LogLevel.debug, msg);
static void infoFrontend(String msg) =>
_instance._log(LogTarget.frontend, LogLevel.info, msg);
static void warnFrontend(String msg) =>
_instance._log(LogTarget.frontend, LogLevel.warn, msg);
static void errorFrontend(String msg) =>
_instance._log(LogTarget.frontend, LogLevel.error, msg);
}
1 change: 1 addition & 0 deletions lib/widgets/lyrics_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ class _SourceBadge extends StatelessWidget {
LyricsSource.embedded => (Icons.music_note, 'Embedded'),
LyricsSource.lrcFile => (Icons.text_snippet_outlined, '.lrc file'),
LyricsSource.txtFile => (Icons.text_fields, '.txt file'),
LyricsSource.lrclib => (Icons.cloud_outlined, 'lrclib'),
LyricsSource.none => (Icons.close, ''),
};

Expand Down
Loading