fix: local tracks takes time to load

This commit is contained in:
Kingkor Roy Tirtho 2024-08-15 22:45:22 +06:00
parent 9294858fb6
commit 470addca83
15 changed files with 377 additions and 270 deletions

View File

@ -7,6 +7,7 @@ import 'package:spotube/collections/routes.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
final appLinks = AppLinks(); final appLinks = AppLinks();
@ -61,30 +62,34 @@ void useDeepLinking(WidgetRef ref) {
} }
final subscription = linkStream.listen((uri) async { final subscription = linkStream.listen((uri) async {
final startSegment = uri.split(":").take(2).join(":"); try {
final endSegment = uri.split(":").last; final startSegment = uri.split(":").take(2).join(":");
final endSegment = uri.split(":").last;
switch (startSegment) { switch (startSegment) {
case "spotify:album": case "spotify:album":
await router.push( await router.push(
"/album/$endSegment", "/album/$endSegment",
extra: await spotify.albums.get(endSegment), extra: await spotify.albums.get(endSegment),
); );
break; break;
case "spotify:artist": case "spotify:artist":
await router.push("/artist/$endSegment"); await router.push("/artist/$endSegment");
break; break;
case "spotify:track": case "spotify:track":
await router.push("/track/$endSegment"); await router.push("/track/$endSegment");
break; break;
case "spotify:playlist": case "spotify:playlist":
await router.push( await router.push(
"/playlist/$endSegment", "/playlist/$endSegment",
extra: await spotify.playlists.get(endSegment), extra: await spotify.playlists.get(endSegment),
); );
break; break;
default: default:
break; break;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}); });

View File

@ -1,6 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
int useSyncedLyrics( int useSyncedLyrics(
WidgetRef ref, WidgetRef ref,
@ -13,8 +14,12 @@ int useSyncedLyrics(
useEffect(() { useEffect(() {
return stream.listen((pos) { return stream.listen((pos) {
if (lyricsMap.containsKey(pos.inSeconds + delay)) { try {
currentTime.value = pos.inSeconds + delay; if (lyricsMap.containsKey(pos.inSeconds + delay)) {
currentTime.value = pos.inSeconds + delay;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}).cancel; }).cancel;
}, [lyricsMap, delay]); }, [lyricsMap, delay]);

View File

@ -17,6 +17,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:stroke_text/stroke_text.dart'; import 'package:stroke_text/stroke_text.dart';
@ -80,12 +81,16 @@ class SyncedLyrics extends HookConsumerWidget {
StreamSubscription? subscription; StreamSubscription? subscription;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
subscription = audioPlayer.positionStream.listen((event) { subscription = audioPlayer.positionStream.listen((event) {
if (event > Duration.zero) return; try {
controller.animateTo( if (event > Duration.zero) return;
0, controller.animateTo(
duration: const Duration(milliseconds: 500), 0,
curve: Curves.easeInOut, duration: const Duration(milliseconds: 500),
); curve: Curves.easeInOut,
);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
}); });

View File

@ -7,6 +7,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/logs/logs_provider.dart'; import 'package:spotube/provider/logs/logs_provider.dart';
import 'package:spotube/services/logger/logger.dart';
class LogsPage extends HookConsumerWidget { class LogsPage extends HookConsumerWidget {
static const name = "logs"; static const name = "logs";
@ -40,6 +41,17 @@ class LogsPage extends HookConsumerWidget {
} }
}, },
), ),
IconButton(
icon: const Icon(SpotubeIcons.trash),
iconSize: 16,
onPressed: () async {
ref.invalidate(logsProvider);
final logsFile = await AppLogger.getLogsPath();
await logsFile.writeAsString("");
},
)
], ],
), ),
body: SafeArea( body: SafeArea(

View File

@ -13,6 +13,7 @@ import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
class AudioPlayerNotifier extends Notifier<AudioPlayerState> { class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier); BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
@ -141,36 +142,52 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
build() { build() {
final subscriptions = [ final subscriptions = [
audioPlayer.playingStream.listen((playing) async { audioPlayer.playingStream.listen((playing) async {
state = state.copyWith(playing: playing); try {
state = state.copyWith(playing: playing);
await _updatePlayerState( await _updatePlayerState(
AudioPlayerStateTableCompanion( AudioPlayerStateTableCompanion(
playing: Value(playing), playing: Value(playing),
), ),
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.loopModeStream.listen((loopMode) async { audioPlayer.loopModeStream.listen((loopMode) async {
state = state.copyWith(loopMode: loopMode); try {
state = state.copyWith(loopMode: loopMode);
await _updatePlayerState( await _updatePlayerState(
AudioPlayerStateTableCompanion( AudioPlayerStateTableCompanion(
loopMode: Value(loopMode), loopMode: Value(loopMode),
), ),
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.shuffledStream.listen((shuffled) async { audioPlayer.shuffledStream.listen((shuffled) async {
state = state.copyWith(shuffled: shuffled); try {
state = state.copyWith(shuffled: shuffled);
await _updatePlayerState( await _updatePlayerState(
AudioPlayerStateTableCompanion( AudioPlayerStateTableCompanion(
shuffled: Value(shuffled), shuffled: Value(shuffled),
), ),
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.playlistStream.listen((playlist) async { audioPlayer.playlistStream.listen((playlist) async {
state = state.copyWith(playlist: playlist); try {
state = state.copyWith(playlist: playlist);
await _updatePlaylist(playlist); await _updatePlaylist(playlist);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
]; ];

View File

@ -73,25 +73,33 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPlaylist() { StreamSubscription subscribeToPlaylist() {
return audioPlayer.playlistStream.listen((mpvPlaylist) { return audioPlayer.playlistStream.listen((mpvPlaylist) {
notificationService.addTrack(audioPlayerState.activeTrack!); try {
discord.updatePresence(audioPlayerState.activeTrack!); notificationService.addTrack(audioPlayerState.activeTrack!);
updatePalette(); discord.updatePresence(audioPlayerState.activeTrack!);
updatePalette();
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
} }
StreamSubscription subscribeToSkipSponsor() { StreamSubscription subscribeToSkipSponsor() {
return audioPlayer.positionStream.listen((position) async { return audioPlayer.positionStream.listen((position) async {
final currentSegments = await ref.read(segmentProvider.future); try {
final currentSegments = await ref.read(segmentProvider.future);
if (currentSegments?.segments.isNotEmpty != true || if (currentSegments?.segments.isNotEmpty != true ||
position < const Duration(seconds: 3)) return; position < const Duration(seconds: 3)) return;
for (final segment in currentSegments!.segments) { for (final segment in currentSegments!.segments) {
final seconds = position.inSeconds; final seconds = position.inSeconds;
if (seconds < segment.start || seconds >= segment.end) continue; if (seconds < segment.start || seconds >= segment.end) continue;
await audioPlayer.seek(Duration(seconds: segment.end + 1)); await audioPlayer.seek(Duration(seconds: segment.end + 1));
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}); });
} }
@ -122,23 +130,28 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPosition() { StreamSubscription subscribeToPosition() {
String lastTrack = ""; // used to prevent multiple calls to the same track String lastTrack = ""; // used to prevent multiple calls to the same track
return audioPlayer.positionStream.listen((event) async { return audioPlayer.positionStream.listen((event) async {
if (event < const Duration(seconds: 3) ||
audioPlayerState.playlist.index == -1 ||
audioPlayerState.playlist.index ==
audioPlayerState.tracks.length - 1) {
return;
}
final nextTrack = SpotubeMedia.fromMedia(audioPlayerState.playlist.medias
.elementAt(audioPlayerState.playlist.index + 1));
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
return;
}
try { try {
await ref.read(sourcedTrackProvider(nextTrack).future); if (event < const Duration(seconds: 3) ||
} finally { audioPlayerState.playlist.index == -1 ||
lastTrack = nextTrack.track.id!; audioPlayerState.playlist.index ==
audioPlayerState.tracks.length - 1) {
return;
}
final nextTrack = SpotubeMedia.fromMedia(audioPlayerState
.playlist.medias
.elementAt(audioPlayerState.playlist.index + 1));
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
return;
}
try {
await ref.read(sourcedTrackProvider(nextTrack).future);
} finally {
lastTrack = nextTrack.track.id!;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}); });
} }

View File

@ -1,6 +1,7 @@
import 'package:bonsoir/bonsoir.dart'; import 'package:bonsoir/bonsoir.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/device_info/device_info.dart'; import 'package:spotube/services/device_info/device_info.dart';
import 'package:spotube/services/logger/logger.dart';
class ConnectClientsState { class ConnectClientsState {
final List<BonsoirService> services; final List<BonsoirService> services;
@ -37,42 +38,47 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
final subscription = discovery.eventStream?.listen((event) { final subscription = discovery.eventStream?.listen((event) {
// ignore device itself // ignore device itself
if (event.service?.attributes["deviceId"] == deviceId) { try {
return; if (event.service?.attributes["deviceId"] == deviceId) {
} return;
}
switch (event.type) { switch (event.type) {
case BonsoirDiscoveryEventType.discoveryServiceFound: case BonsoirDiscoveryEventType.discoveryServiceFound:
state = AsyncData(state.value!.copyWith( state = AsyncData(state.value!.copyWith(
services: [ services: [
...?state.value?.services, ...?state.value?.services,
event.service!, event.service!,
], ],
)); ));
break; break;
case BonsoirDiscoveryEventType.discoveryServiceResolved: case BonsoirDiscoveryEventType.discoveryServiceResolved:
state = AsyncData( state = AsyncData(
state.value!.copyWith( state.value!.copyWith(
resolvedService: event.service as ResolvedBonsoirService, resolvedService: event.service as ResolvedBonsoirService,
), ),
); );
break; break;
case BonsoirDiscoveryEventType.discoveryServiceLost: case BonsoirDiscoveryEventType.discoveryServiceLost:
state = AsyncData( state = AsyncData(
ConnectClientsState( ConnectClientsState(
services: state.value!.services services: state.value!.services
.where((s) => s.name != event.service!.name) .where((s) => s.name != event.service!.name)
.toList(), .toList(),
discovery: state.value!.discovery, discovery: state.value!.discovery,
resolvedService: state.value?.resolvedService != null && resolvedService: state.value?.resolvedService != null &&
event.service?.name == state.value?.resolvedService?.name event.service?.name ==
? null state.value?.resolvedService?.name
: state.value!.resolvedService, ? null
), : state.value!.resolvedService,
); ),
break; );
default: break;
break; default:
break;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}); });

View File

@ -7,11 +7,14 @@ import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class DiscordNotifier extends AsyncNotifier<void> { class DiscordNotifier extends AsyncNotifier<void> {
@override @override
FutureOr<void> build() async { FutureOr<void> build() async {
if (!kIsDesktop) return;
final enabled = ref.watch( final enabled = ref.watch(
userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop)); userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop));
@ -19,26 +22,38 @@ class DiscordNotifier extends AsyncNotifier<void> {
final subscriptions = [ final subscriptions = [
FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async {
final playback = ref.read(audioPlayerProvider); try {
if (connected && playback.activeTrack != null) { final playback = ref.read(audioPlayerProvider);
await updatePresence(playback.activeTrack!); if (connected && playback.activeTrack != null) {
await updatePresence(playback.activeTrack!);
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}), }),
audioPlayer.playerStateStream.listen((state) async { audioPlayer.playerStateStream.listen((state) async {
final playback = ref.read(audioPlayerProvider); try {
if (playback.activeTrack == null) return; final playback = ref.read(audioPlayerProvider);
if (playback.activeTrack == null) return;
await updatePresence(ref.read(audioPlayerProvider).activeTrack!); await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.positionStream.listen((position) async { audioPlayer.positionStream.listen((position) async {
final playback = ref.read(audioPlayerProvider); try {
if (playback.activeTrack != null) { final playback = ref.read(audioPlayerProvider);
final diff = position.inMilliseconds - lastPosition.inMilliseconds; if (playback.activeTrack != null) {
if (diff > 500 || diff < -500) { final diff = position.inMilliseconds - lastPosition.inMilliseconds;
await updatePresence(ref.read(audioPlayerProvider).activeTrack!); if (diff > 500 || diff < -500) {
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
}
} }
lastPosition = position;
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
lastPosition = position;
}) })
]; ];
@ -59,6 +74,7 @@ class DiscordNotifier extends AsyncNotifier<void> {
} }
Future<void> updatePresence(Track track) async { Future<void> updatePresence(Track track) async {
if (!kIsDesktop) return;
final artistNames = track.artists?.asString(); final artistNames = track.artists?.asString();
final isPlaying = audioPlayer.isPlaying; final isPlaying = audioPlayer.isPlaying;
final position = audioPlayer.position; final position = audioPlayer.position;
@ -92,10 +108,12 @@ class DiscordNotifier extends AsyncNotifier<void> {
} }
Future<void> clear() async { Future<void> clear() async {
if (!kIsDesktop) return;
await FlutterDiscordRPC.instance.clearActivity(); await FlutterDiscordRPC.instance.clearActivity();
} }
Future<void> close() async { Future<void> close() async {
if (!kIsDesktop) return;
await FlutterDiscordRPC.instance.disconnect(); await FlutterDiscordRPC.instance.disconnect();
} }
} }

View File

@ -23,68 +23,72 @@ class DownloadManagerProvider extends ChangeNotifier {
$backHistory = <Track>{}, $backHistory = <Track>{},
dl = DownloadManager() { dl = DownloadManager() {
dl.statusStream.listen((event) async { dl.statusStream.listen((event) async {
final (:request, :status) = event; try {
final (:request, :status) = event;
final track = $history.firstWhereOrNull( final track = $history.firstWhereOrNull(
(element) => element.getUrlOfCodec(downloadCodec) == request.url, (element) => element.getUrlOfCodec(downloadCodec) == request.url,
); );
if (track == null) return; if (track == null) return;
final savePath = getTrackFileUrl(track); final savePath = getTrackFileUrl(track);
// related to onFileExists // related to onFileExists
final oldFile = File("$savePath.old"); final oldFile = File("$savePath.old");
// if download failed and old file exists, rename it back // if download failed and old file exists, rename it back
if ((status == DownloadStatus.failed || if ((status == DownloadStatus.failed ||
status == DownloadStatus.canceled) && status == DownloadStatus.canceled) &&
await oldFile.exists()) { await oldFile.exists()) {
await oldFile.rename(savePath); await oldFile.rename(savePath);
}
if (status != DownloadStatus.completed ||
//? WebA audiotagging is not supported yet
//? Although in future by converting weba to opus & then tagging it
//? is possible using vorbis comments
downloadCodec == SourceCodecs.weba) return;
final file = File(request.path);
if (await oldFile.exists()) {
await oldFile.delete();
}
final imageBytes = await downloadImage(
(track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
index: 1,
),
);
final metadata = Metadata(
title: track.name,
artist: track.artists?.map((a) => a.name).join(", "),
album: track.album?.name,
albumArtist: track.artists?.map((a) => a.name).join(", "),
year: track.album?.releaseDate != null
? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969
: 1969,
trackNumber: track.trackNumber,
discNumber: track.discNumber,
durationMs: track.durationMs?.toDouble() ?? 0.0,
fileSize: BigInt.from(await file.length()),
trackTotal: track.album?.tracks?.length ?? 0,
picture: imageBytes != null
? Picture(
data: imageBytes,
// Spotify images are always JPEGs
mimeType: 'image/jpeg',
)
: null,
);
await MetadataGod.writeMetadata(
file: file.path,
metadata: metadata,
);
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
if (status != DownloadStatus.completed ||
//? WebA audiotagging is not supported yet
//? Although in future by converting weba to opus & then tagging it
//? is possible using vorbis comments
downloadCodec == SourceCodecs.weba) return;
final file = File(request.path);
if (await oldFile.exists()) {
await oldFile.delete();
}
final imageBytes = await downloadImage(
(track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
index: 1,
),
);
final metadata = Metadata(
title: track.name,
artist: track.artists?.map((a) => a.name).join(", "),
album: track.album?.name,
albumArtist: track.artists?.map((a) => a.name).join(", "),
year: track.album?.releaseDate != null
? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969
: 1969,
trackNumber: track.trackNumber,
discNumber: track.discNumber,
durationMs: track.durationMs?.toDouble() ?? 0.0,
fileSize: BigInt.from(await file.length()),
trackTotal: track.album?.tracks?.length ?? 0,
picture: imageBytes != null
? Picture(
data: imageBytes,
// Spotify images are always JPEGs
mimeType: 'image/jpeg',
)
: null,
);
await MetadataGod.writeMetadata(
file: file.path,
metadata: metadata,
);
}); });
} }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -72,39 +73,35 @@ final localTracksProvider =
} }
} }
final List<Map<dynamic, dynamic>> filesWithMetadata = []; final List<Map<dynamic, dynamic>> filesWithMetadata = await Future.wait(
entities.map((file) async {
try {
final metadata = await MetadataGod.readMetadata(file: file.path);
for (final file in entities) { final imageFile = File(join(
try { (await getTemporaryDirectory()).path,
final metadata = await MetadataGod.readMetadata(file: file.path); "spotube",
basenameWithoutExtension(file.path) +
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
));
if (!await imageFile.exists() && metadata.picture != null) {
await imageFile.create(recursive: true);
await imageFile.writeAsBytes(
metadata.picture?.data ?? [],
mode: FileMode.writeOnly,
);
}
await Future.delayed(const Duration(milliseconds: 50)); return {"metadata": metadata, "file": file, "art": imageFile.path};
} catch (e, stack) {
final imageFile = File(join( if (e case FrbException() || TimeoutException()) {
(await getTemporaryDirectory()).path, return {"file": file};
"spotube", }
basenameWithoutExtension(file.path) + AppLogger.reportError(e, stack);
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, return null;
));
if (!await imageFile.exists() && metadata.picture != null) {
await imageFile.create(recursive: true);
await imageFile.writeAsBytes(
metadata.picture?.data ?? [],
mode: FileMode.writeOnly,
);
} }
}),
filesWithMetadata.add( ).then((value) => value.whereNotNull().toList());
{"metadata": metadata, "file": file, "art": imageFile.path},
);
} catch (e, stack) {
if (e case FrbException() || TimeoutException()) {
filesWithMetadata.add({"file": file});
}
AppLogger.reportError(e, stack);
continue;
}
}
final tracksFromMetadata = filesWithMetadata final tracksFromMetadata = filesWithMetadata
.map( .map(

View File

@ -23,19 +23,23 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
final subscription = final subscription =
database.select(database.scrobblerTable).watch().listen((event) async { database.select(database.scrobblerTable).watch().listen((event) async {
if (event.isNotEmpty) { try {
state = await AsyncValue.guard( if (event.isNotEmpty) {
() async => Scrobblenaut( state = await AsyncValue.guard(
lastFM: await LastFM.authenticateWithPasswordHash( () async => Scrobblenaut(
apiKey: Env.lastFmApiKey, lastFM: await LastFM.authenticateWithPasswordHash(
apiSecret: Env.lastFmApiSecret, apiKey: Env.lastFmApiKey,
username: event.first.username, apiSecret: Env.lastFmApiSecret,
passwordHash: event.first.passwordHash.value, username: event.first.username,
passwordHash: event.first.passwordHash.value,
),
), ),
), );
); } else {
} else { state = const AsyncValue.data(null);
state = const AsyncValue.data(null); }
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}); });

View File

@ -10,6 +10,7 @@ import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -41,15 +42,21 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
..where((tbl) => tbl.id.equals(0))) ..where((tbl) => tbl.id.equals(0)))
.watchSingle() .watchSingle()
.listen((event) async { .listen((event) async {
state = event; try {
state = event;
if (kIsDesktop) { if (kIsDesktop) {
await windowManager.setTitleBarStyle( await windowManager.setTitleBarStyle(
state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, state.systemTitleBar
); ? TitleBarStyle.normal
: TitleBarStyle.hidden,
);
}
await audioPlayer.setAudioNormalization(state.normalizeAudio);
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
await audioPlayer.setAudioNormalization(state.normalizeAudio);
}); });
ref.onDispose(() { ref.onDispose(() {

View File

@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart';
import 'package:spotube/services/audio_services/windows_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -30,8 +31,8 @@ class AudioServices with WidgetsBindingObserver {
kIsLinux ? 'spotube' : 'com.krtirtho.Spotube', kIsLinux ? 'spotube' : 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube', androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: false, androidNotificationOngoing: false,
androidNotificationIcon: "drawable/ic_launcher_monochrome",
androidStopForegroundOnPause: false, androidStopForegroundOnPause: false,
androidNotificationIcon: "drawable/ic_launcher_monochrome",
androidNotificationChannelDescription: "Spotube Media Controls", androidNotificationChannelDescription: "Spotube Media Controls",
), ),
) )

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart'; import 'package:audio_session/audio_session.dart';
@ -6,6 +7,8 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart';
class MobileAudioService extends BaseAudioHandler { class MobileAudioService extends BaseAudioHandler {
AudioSession? session; AudioSession? session;
@ -120,35 +123,40 @@ class MobileAudioService extends BaseAudioHandler {
@override @override
Future<void> onTaskRemoved() async { Future<void> onTaskRemoved() async {
await audioPlayerNotifier.stop(); await audioPlayerNotifier.stop();
return super.onTaskRemoved(); if (kIsAndroid) exit(0);
} }
Future<PlaybackState> _transformEvent() async { Future<PlaybackState> _transformEvent() async {
return PlaybackState( try {
controls: [ return PlaybackState(
MediaControl.skipToPrevious, controls: [
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play, MediaControl.skipToPrevious,
MediaControl.skipToNext, audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
MediaControl.stop, MediaControl.skipToNext,
], MediaControl.stop,
systemActions: { ],
MediaAction.seek, systemActions: {
}, MediaAction.seek,
androidCompactActionIndices: const [0, 1, 2], },
playing: audioPlayer.isPlaying, androidCompactActionIndices: const [0, 1, 2],
updatePosition: audioPlayer.position, playing: audioPlayer.isPlaying,
bufferedPosition: audioPlayer.bufferedPosition, updatePosition: audioPlayer.position,
shuffleMode: audioPlayer.isShuffled == true bufferedPosition: audioPlayer.bufferedPosition,
? AudioServiceShuffleMode.all shuffleMode: audioPlayer.isShuffled == true
: AudioServiceShuffleMode.none, ? AudioServiceShuffleMode.all
repeatMode: switch (audioPlayer.loopMode) { : AudioServiceShuffleMode.none,
PlaylistMode.loop => AudioServiceRepeatMode.all, repeatMode: switch (audioPlayer.loopMode) {
PlaylistMode.single => AudioServiceRepeatMode.one, PlaylistMode.loop => AudioServiceRepeatMode.all,
_ => AudioServiceRepeatMode.none, PlaylistMode.single => AudioServiceRepeatMode.one,
}, _ => AudioServiceRepeatMode.none,
processingState: audioPlayer.isBuffering },
? AudioProcessingState.loading processingState: audioPlayer.isBuffering
: AudioProcessingState.ready, ? AudioProcessingState.loading
); : AudioProcessingState.ready,
);
} catch (e, stack) {
AppLogger.reportError(e, stack);
rethrow;
}
} }
} }

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:spotube/services/logger/logger.dart';
class ConnectionCheckerService with WidgetsBindingObserver { class ConnectionCheckerService with WidgetsBindingObserver {
final _connectionStreamController = StreamController<bool>.broadcast(); final _connectionStreamController = StreamController<bool>.broadcast();
@ -16,17 +17,21 @@ class ConnectionCheckerService with WidgetsBindingObserver {
Timer? timer; Timer? timer;
onConnectivityChanged.listen((connected) { onConnectivityChanged.listen((connected) {
if (!connected && timer == null) { try {
timer = Timer.periodic(const Duration(seconds: 30), (timer) async { if (!connected && timer == null) {
if (WidgetsBinding.instance.lifecycleState == timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
AppLifecycleState.paused) { if (WidgetsBinding.instance.lifecycleState ==
return; AppLifecycleState.paused) {
} return;
await isConnected; }
}); await isConnected;
} else { });
timer?.cancel(); } else {
timer = null; timer?.cancel();
timer = null;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
} }
}); });
} }