diff --git a/lib/main.dart b/lib/main.dart index 437388d..1783b81 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/audio_player_stream.dart'; import 'package:rhythm_box/providers/auth.dart'; import 'package:rhythm_box/providers/database.dart'; +import 'package:rhythm_box/providers/endless_playback.dart'; import 'package:rhythm_box/providers/history.dart'; import 'package:rhythm_box/providers/palette.dart'; import 'package:rhythm_box/providers/scrobbler.dart'; @@ -92,6 +93,7 @@ class MyApp extends StatelessWidget { Get.put(AudioPlayerProvider()); Get.put(ActiveSourcedTrackProvider()); Get.put(AudioPlayerStreamProvider()); + Get.put(EndlessPlaybackProvider()); Get.put(PlaybackHistoryProvider()); Get.put(SegmentsProvider()); diff --git a/lib/providers/endless_playback.dart b/lib/providers/endless_playback.dart new file mode 100644 index 0000000..b6e9392 --- /dev/null +++ b/lib/providers/endless_playback.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/providers/auth.dart'; +import 'package:rhythm_box/providers/spotify.dart'; +import 'package:rhythm_box/providers/user_preferences.dart'; +import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:spotify/spotify.dart'; + +class EndlessPlaybackProvider extends GetxController { + late final _auth = Get.find(); + late final _playback = Get.find(); + late final _spotify = Get.find().api; + late final _preferences = Get.find(); + + bool get isEndlessPlayback => _preferences.state.value.endlessPlayback; + + late final StreamSubscription _subscription; + + StreamSubscription? _idxSubscription; + + @override + void onInit() { + super.onInit(); + + _initPlayback(); + + _subscription = _preferences.state.listen((value) { + if (value.endlessPlayback && _idxSubscription == null) { + _initPlayback(); + } else if (!value.endlessPlayback && _idxSubscription != null) { + _idxSubscription!.cancel(); + _idxSubscription = null; + } + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + void _initPlayback() { + if (!isEndlessPlayback || _auth.auth.value == null) return; + + void listener(int index) async { + try { + final playState = _playback.state.value; + if (index != playState.tracks.length - 1) return; + + final track = playState.tracks.last; + + final query = '${track.name} Radio'; + final pages = await _spotify.search + .get(query, types: [SearchType.playlist]).first(); + + final radios = pages + .expand((e) => e.items?.toList() ?? []) + .toList() + .cast(); + + final artists = track.artists!.map((e) => e.name); + + final radio = radios.firstWhere( + (e) { + final validPlaylists = + artists.where((a) => e.description!.contains(a!)); + return e.name == '${track.name} Radio' && + (validPlaylists.length >= 2 || + validPlaylists.length == artists.length) && + e.owner?.displayName != 'Spotify'; + }, + orElse: () => radios.first, + ); + + final tracks = + await _spotify.playlists.getTracksByPlaylistId(radio.id!).all(); + + await _playback.addTracks( + tracks.toList() + ..removeWhere((e) { + final isDuplicate = + _playback.state.value.tracks.any((t) => t.id == e.id); + return e.id == track.id || isDuplicate; + }), + ); + } catch (e, stack) { + log('[EndlessPlayback] Error: $e; Trace:\n$stack'); + } + } + + if (_playback.state.value.playlist.index == + _playback.state.value.playlist.medias.length - 1 && + _playback.isPlaying.value) { + listener(_playback.state.value.playlist.index); + } + + _idxSubscription = audioPlayer.currentIndexChangedStream.listen(listener); + } +} diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index ff77011..12d4c89 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -100,6 +100,17 @@ class _SettingsScreenState extends State { ); }), const Divider(thickness: 0.3, height: 1), + Obx( + () => SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + secondary: const Icon(Icons.all_inclusive), + title: const Text('Endless Playback'), + subtitle: const Text( + 'Automatically get more recommendation for you after your queue finish playing'), + value: _preferences.state.value.endlessPlayback, + onChanged: _preferences.setEndlessPlayback, + ), + ), Obx( () => SwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), diff --git a/lib/widgets/lyrics/synced_lyrics.dart b/lib/widgets/lyrics/synced_lyrics.dart index 6a38f3f..e24b3d7 100644 --- a/lib/widgets/lyrics/synced_lyrics.dart +++ b/lib/widgets/lyrics/synced_lyrics.dart @@ -63,8 +63,16 @@ class _SyncedLyricsState extends State { idx, preferPosition: AutoScrollPosition.middle, ); + return; } } + + if (_lyric!.lyrics.isNotEmpty) { + _autoScrollController.scrollToIndex( + 0, + preferPosition: AutoScrollPosition.begin, + ); + } } @override @@ -77,7 +85,9 @@ class _SyncedLyricsState extends State { _playback.state.listen((value) { if (value.activeTrack == null) return; if (value.activeTrack!.id != _activeTrackId) { - _pullLyrics(); + _pullLyrics().then((_) { + _syncLyricsProgress(); + }); } }), ];