mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
fix: local tracks takes time to load
This commit is contained in:
parent
9294858fb6
commit
470addca83
@ -7,6 +7,7 @@ import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final appLinks = AppLinks();
|
||||
@ -61,30 +62,34 @@ void useDeepLinking(WidgetRef ref) {
|
||||
}
|
||||
|
||||
final subscription = linkStream.listen((uri) async {
|
||||
final startSegment = uri.split(":").take(2).join(":");
|
||||
final endSegment = uri.split(":").last;
|
||||
try {
|
||||
final startSegment = uri.split(":").take(2).join(":");
|
||||
final endSegment = uri.split(":").last;
|
||||
|
||||
switch (startSegment) {
|
||||
case "spotify:album":
|
||||
await router.push(
|
||||
"/album/$endSegment",
|
||||
extra: await spotify.albums.get(endSegment),
|
||||
);
|
||||
break;
|
||||
case "spotify:artist":
|
||||
await router.push("/artist/$endSegment");
|
||||
break;
|
||||
case "spotify:track":
|
||||
await router.push("/track/$endSegment");
|
||||
break;
|
||||
case "spotify:playlist":
|
||||
await router.push(
|
||||
"/playlist/$endSegment",
|
||||
extra: await spotify.playlists.get(endSegment),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
switch (startSegment) {
|
||||
case "spotify:album":
|
||||
await router.push(
|
||||
"/album/$endSegment",
|
||||
extra: await spotify.albums.get(endSegment),
|
||||
);
|
||||
break;
|
||||
case "spotify:artist":
|
||||
await router.push("/artist/$endSegment");
|
||||
break;
|
||||
case "spotify:track":
|
||||
await router.push("/track/$endSegment");
|
||||
break;
|
||||
case "spotify:playlist":
|
||||
await router.push(
|
||||
"/playlist/$endSegment",
|
||||
extra: await spotify.playlists.get(endSegment),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
int useSyncedLyrics(
|
||||
WidgetRef ref,
|
||||
@ -13,8 +14,12 @@ int useSyncedLyrics(
|
||||
|
||||
useEffect(() {
|
||||
return stream.listen((pos) {
|
||||
if (lyricsMap.containsKey(pos.inSeconds + delay)) {
|
||||
currentTime.value = pos.inSeconds + delay;
|
||||
try {
|
||||
if (lyricsMap.containsKey(pos.inSeconds + delay)) {
|
||||
currentTime.value = pos.inSeconds + delay;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}).cancel;
|
||||
}, [lyricsMap, delay]);
|
||||
|
@ -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/spotify/spotify.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
import 'package:stroke_text/stroke_text.dart';
|
||||
|
||||
@ -80,12 +81,16 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
StreamSubscription? subscription;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
subscription = audioPlayer.positionStream.listen((event) {
|
||||
if (event > Duration.zero) return;
|
||||
controller.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
try {
|
||||
if (event > Duration.zero) return;
|
||||
controller.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -7,6 +7,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/logs/logs_provider.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class LogsPage extends HookConsumerWidget {
|
||||
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(
|
||||
|
@ -13,6 +13,7 @@ import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/discord_provider.dart';
|
||||
import 'package:spotube/provider/server/sourced_track.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
|
||||
@ -141,36 +142,52 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
build() {
|
||||
final subscriptions = [
|
||||
audioPlayer.playingStream.listen((playing) async {
|
||||
state = state.copyWith(playing: playing);
|
||||
try {
|
||||
state = state.copyWith(playing: playing);
|
||||
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
playing: Value(playing),
|
||||
),
|
||||
);
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
playing: Value(playing),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.loopModeStream.listen((loopMode) async {
|
||||
state = state.copyWith(loopMode: loopMode);
|
||||
try {
|
||||
state = state.copyWith(loopMode: loopMode);
|
||||
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
loopMode: Value(loopMode),
|
||||
),
|
||||
);
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
loopMode: Value(loopMode),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.shuffledStream.listen((shuffled) async {
|
||||
state = state.copyWith(shuffled: shuffled);
|
||||
try {
|
||||
state = state.copyWith(shuffled: shuffled);
|
||||
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
shuffled: Value(shuffled),
|
||||
),
|
||||
);
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
shuffled: Value(shuffled),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
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);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
|
@ -73,25 +73,33 @@ class AudioPlayerStreamListeners {
|
||||
|
||||
StreamSubscription subscribeToPlaylist() {
|
||||
return audioPlayer.playlistStream.listen((mpvPlaylist) {
|
||||
notificationService.addTrack(audioPlayerState.activeTrack!);
|
||||
discord.updatePresence(audioPlayerState.activeTrack!);
|
||||
updatePalette();
|
||||
try {
|
||||
notificationService.addTrack(audioPlayerState.activeTrack!);
|
||||
discord.updatePresence(audioPlayerState.activeTrack!);
|
||||
updatePalette();
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription subscribeToSkipSponsor() {
|
||||
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 ||
|
||||
position < const Duration(seconds: 3)) return;
|
||||
if (currentSegments?.segments.isNotEmpty != true ||
|
||||
position < const Duration(seconds: 3)) return;
|
||||
|
||||
for (final segment in currentSegments!.segments) {
|
||||
final seconds = position.inSeconds;
|
||||
for (final segment in currentSegments!.segments) {
|
||||
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() {
|
||||
String lastTrack = ""; // used to prevent multiple calls to the same track
|
||||
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 {
|
||||
await ref.read(sourcedTrackProvider(nextTrack).future);
|
||||
} finally {
|
||||
lastTrack = nextTrack.track.id!;
|
||||
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 {
|
||||
await ref.read(sourcedTrackProvider(nextTrack).future);
|
||||
} finally {
|
||||
lastTrack = nextTrack.track.id!;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bonsoir/bonsoir.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/services/device_info/device_info.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class ConnectClientsState {
|
||||
final List<BonsoirService> services;
|
||||
@ -37,42 +38,47 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
|
||||
|
||||
final subscription = discovery.eventStream?.listen((event) {
|
||||
// ignore device itself
|
||||
if (event.service?.attributes["deviceId"] == deviceId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (event.service?.attributes["deviceId"] == deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case BonsoirDiscoveryEventType.discoveryServiceFound:
|
||||
state = AsyncData(state.value!.copyWith(
|
||||
services: [
|
||||
...?state.value?.services,
|
||||
event.service!,
|
||||
],
|
||||
));
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceResolved:
|
||||
state = AsyncData(
|
||||
state.value!.copyWith(
|
||||
resolvedService: event.service as ResolvedBonsoirService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceLost:
|
||||
state = AsyncData(
|
||||
ConnectClientsState(
|
||||
services: state.value!.services
|
||||
.where((s) => s.name != event.service!.name)
|
||||
.toList(),
|
||||
discovery: state.value!.discovery,
|
||||
resolvedService: state.value?.resolvedService != null &&
|
||||
event.service?.name == state.value?.resolvedService?.name
|
||||
? null
|
||||
: state.value!.resolvedService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
switch (event.type) {
|
||||
case BonsoirDiscoveryEventType.discoveryServiceFound:
|
||||
state = AsyncData(state.value!.copyWith(
|
||||
services: [
|
||||
...?state.value?.services,
|
||||
event.service!,
|
||||
],
|
||||
));
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceResolved:
|
||||
state = AsyncData(
|
||||
state.value!.copyWith(
|
||||
resolvedService: event.service as ResolvedBonsoirService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceLost:
|
||||
state = AsyncData(
|
||||
ConnectClientsState(
|
||||
services: state.value!.services
|
||||
.where((s) => s.name != event.service!.name)
|
||||
.toList(),
|
||||
discovery: state.value!.discovery,
|
||||
resolvedService: state.value?.resolvedService != null &&
|
||||
event.service?.name ==
|
||||
state.value?.resolvedService?.name
|
||||
? null
|
||||
: state.value!.resolvedService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -7,11 +7,14 @@ import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class DiscordNotifier extends AsyncNotifier<void> {
|
||||
@override
|
||||
FutureOr<void> build() async {
|
||||
if (!kIsDesktop) return;
|
||||
|
||||
final enabled = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop));
|
||||
|
||||
@ -19,26 +22,38 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
||||
|
||||
final subscriptions = [
|
||||
FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (connected && playback.activeTrack != null) {
|
||||
await updatePresence(playback.activeTrack!);
|
||||
try {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (connected && playback.activeTrack != null) {
|
||||
await updatePresence(playback.activeTrack!);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.playerStateStream.listen((state) async {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack == null) return;
|
||||
try {
|
||||
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 {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack != null) {
|
||||
final diff = position.inMilliseconds - lastPosition.inMilliseconds;
|
||||
if (diff > 500 || diff < -500) {
|
||||
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
|
||||
try {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack != null) {
|
||||
final diff = position.inMilliseconds - lastPosition.inMilliseconds;
|
||||
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 {
|
||||
if (!kIsDesktop) return;
|
||||
final artistNames = track.artists?.asString();
|
||||
final isPlaying = audioPlayer.isPlaying;
|
||||
final position = audioPlayer.position;
|
||||
@ -92,10 +108,12 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
if (!kIsDesktop) return;
|
||||
await FlutterDiscordRPC.instance.clearActivity();
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
if (!kIsDesktop) return;
|
||||
await FlutterDiscordRPC.instance.disconnect();
|
||||
}
|
||||
}
|
||||
|
@ -23,68 +23,72 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
$backHistory = <Track>{},
|
||||
dl = DownloadManager() {
|
||||
dl.statusStream.listen((event) async {
|
||||
final (:request, :status) = event;
|
||||
try {
|
||||
final (:request, :status) = event;
|
||||
|
||||
final track = $history.firstWhereOrNull(
|
||||
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
||||
);
|
||||
if (track == null) return;
|
||||
final track = $history.firstWhereOrNull(
|
||||
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
||||
);
|
||||
if (track == null) return;
|
||||
|
||||
final savePath = getTrackFileUrl(track);
|
||||
// related to onFileExists
|
||||
final oldFile = File("$savePath.old");
|
||||
final savePath = getTrackFileUrl(track);
|
||||
// related to onFileExists
|
||||
final oldFile = File("$savePath.old");
|
||||
|
||||
// if download failed and old file exists, rename it back
|
||||
if ((status == DownloadStatus.failed ||
|
||||
status == DownloadStatus.canceled) &&
|
||||
await oldFile.exists()) {
|
||||
await oldFile.rename(savePath);
|
||||
// if download failed and old file exists, rename it back
|
||||
if ((status == DownloadStatus.failed ||
|
||||
status == DownloadStatus.canceled) &&
|
||||
await oldFile.exists()) {
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:flutter/foundation.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) {
|
||||
try {
|
||||
final metadata = await MetadataGod.readMetadata(file: file.path);
|
||||
final imageFile = File(join(
|
||||
(await getTemporaryDirectory()).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));
|
||||
|
||||
final imageFile = File(join(
|
||||
(await getTemporaryDirectory()).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,
|
||||
);
|
||||
return {"metadata": metadata, "file": file, "art": imageFile.path};
|
||||
} catch (e, stack) {
|
||||
if (e case FrbException() || TimeoutException()) {
|
||||
return {"file": file};
|
||||
}
|
||||
AppLogger.reportError(e, stack);
|
||||
return null;
|
||||
}
|
||||
|
||||
filesWithMetadata.add(
|
||||
{"metadata": metadata, "file": file, "art": imageFile.path},
|
||||
);
|
||||
} catch (e, stack) {
|
||||
if (e case FrbException() || TimeoutException()) {
|
||||
filesWithMetadata.add({"file": file});
|
||||
}
|
||||
AppLogger.reportError(e, stack);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}),
|
||||
).then((value) => value.whereNotNull().toList());
|
||||
|
||||
final tracksFromMetadata = filesWithMetadata
|
||||
.map(
|
||||
|
@ -23,19 +23,23 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
||||
|
||||
final subscription =
|
||||
database.select(database.scrobblerTable).watch().listen((event) async {
|
||||
if (event.isNotEmpty) {
|
||||
state = await AsyncValue.guard(
|
||||
() async => Scrobblenaut(
|
||||
lastFM: await LastFM.authenticateWithPasswordHash(
|
||||
apiKey: Env.lastFmApiKey,
|
||||
apiSecret: Env.lastFmApiSecret,
|
||||
username: event.first.username,
|
||||
passwordHash: event.first.passwordHash.value,
|
||||
try {
|
||||
if (event.isNotEmpty) {
|
||||
state = await AsyncValue.guard(
|
||||
() async => Scrobblenaut(
|
||||
lastFM: await LastFM.authenticateWithPasswordHash(
|
||||
apiKey: Env.lastFmApiKey,
|
||||
apiSecret: Env.lastFmApiSecret,
|
||||
username: event.first.username,
|
||||
passwordHash: event.first.passwordHash.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
state = const AsyncValue.data(null);
|
||||
);
|
||||
} else {
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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/palette_provider.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/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@ -41,15 +42,21 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
..where((tbl) => tbl.id.equals(0)))
|
||||
.watchSingle()
|
||||
.listen((event) async {
|
||||
state = event;
|
||||
try {
|
||||
state = event;
|
||||
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setTitleBarStyle(
|
||||
state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
|
||||
);
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setTitleBarStyle(
|
||||
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(() {
|
||||
|
@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/extensions/image.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/windows_audio_service.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
@ -30,8 +31,8 @@ class AudioServices with WidgetsBindingObserver {
|
||||
kIsLinux ? 'spotube' : 'com.krtirtho.Spotube',
|
||||
androidNotificationChannelName: 'Spotube',
|
||||
androidNotificationOngoing: false,
|
||||
androidNotificationIcon: "drawable/ic_launcher_monochrome",
|
||||
androidStopForegroundOnPause: false,
|
||||
androidNotificationIcon: "drawable/ic_launcher_monochrome",
|
||||
androidNotificationChannelDescription: "Spotube Media Controls",
|
||||
),
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audio_service/audio_service.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/services/audio_player/audio_player.dart';
|
||||
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 {
|
||||
AudioSession? session;
|
||||
@ -120,35 +123,40 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
@override
|
||||
Future<void> onTaskRemoved() async {
|
||||
await audioPlayerNotifier.stop();
|
||||
return super.onTaskRemoved();
|
||||
if (kIsAndroid) exit(0);
|
||||
}
|
||||
|
||||
Future<PlaybackState> _transformEvent() async {
|
||||
return PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
MediaControl.stop,
|
||||
],
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: audioPlayer.position,
|
||||
bufferedPosition: audioPlayer.bufferedPosition,
|
||||
shuffleMode: audioPlayer.isShuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: switch (audioPlayer.loopMode) {
|
||||
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
},
|
||||
processingState: audioPlayer.isBuffering
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
);
|
||||
try {
|
||||
return PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
MediaControl.stop,
|
||||
],
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: audioPlayer.position,
|
||||
bufferedPosition: audioPlayer.bufferedPosition,
|
||||
shuffleMode: audioPlayer.isShuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: switch (audioPlayer.loopMode) {
|
||||
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
},
|
||||
processingState: audioPlayer.isBuffering
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
final _connectionStreamController = StreamController<bool>.broadcast();
|
||||
@ -16,17 +17,21 @@ class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
Timer? timer;
|
||||
|
||||
onConnectivityChanged.listen((connected) {
|
||||
if (!connected && timer == null) {
|
||||
timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (WidgetsBinding.instance.lifecycleState ==
|
||||
AppLifecycleState.paused) {
|
||||
return;
|
||||
}
|
||||
await isConnected;
|
||||
});
|
||||
} else {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
try {
|
||||
if (!connected && timer == null) {
|
||||
timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (WidgetsBinding.instance.lifecycleState ==
|
||||
AppLifecycleState.paused) {
|
||||
return;
|
||||
}
|
||||
await isConnected;
|
||||
});
|
||||
} else {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user