mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +00:00
feat: Implemented continuous playing
This commit is contained in:
parent
2c3aebc83b
commit
176db09662
9 changed files with 101 additions and 44 deletions
|
|
@ -1,13 +1,9 @@
|
||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
|
||||||
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
|
||||||
|
|
@ -54,12 +50,12 @@ public class SongRepository {
|
||||||
return starredSongs;
|
return starredSongs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getInstantMix(Child song, int count) {
|
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
|
||||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getBrowsingClient()
|
.getBrowsingClient()
|
||||||
.getSimilarSongs2(song.getId(), count)
|
.getSimilarSongs2(id, count)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
package com.cappielloantonio.tempo.service;
|
package com.cappielloantonio.tempo.service;
|
||||||
|
|
||||||
import androidx.media3.common.MediaItem;
|
import android.content.ComponentName;
|
||||||
import androidx.media3.session.MediaBrowser;
|
|
||||||
|
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
|
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
|
||||||
import com.cappielloantonio.tempo.model.Chronology;
|
import com.cappielloantonio.tempo.model.Chronology;
|
||||||
import com.cappielloantonio.tempo.repository.ChronologyRepository;
|
import com.cappielloantonio.tempo.repository.ChronologyRepository;
|
||||||
|
|
@ -299,6 +307,30 @@ public class MediaManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public static void continuousPlay(MediaItem mediaItem) {
|
||||||
|
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
|
||||||
|
Preferences.setLastInstantMix();
|
||||||
|
|
||||||
|
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
|
||||||
|
instantMix.observeForever(new Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> media) {
|
||||||
|
if (media != null) {
|
||||||
|
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
|
||||||
|
App.getContext(),
|
||||||
|
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))
|
||||||
|
).buildAsync();
|
||||||
|
|
||||||
|
enqueue(mediaBrowserListenableFuture, media, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
instantMix.removeObserver(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void saveChronology(MediaItem mediaItem) {
|
public static void saveChronology(MediaItem mediaItem) {
|
||||||
if (mediaItem != null) {
|
if (mediaItem != null) {
|
||||||
getChronologyRepository().insert(new Chronology(mediaItem));
|
getChronologyRepository().insert(new Chronology(mediaItem));
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ object Preferences {
|
||||||
private const val HOME_SECTOR_LIST = "home_sector_list"
|
private const val HOME_SECTOR_LIST = "home_sector_list"
|
||||||
private const val RATING_PER_ITEM = "rating_per_item"
|
private const val RATING_PER_ITEM = "rating_per_item"
|
||||||
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
||||||
|
private const val CONTINUOUS_PLAY = "continuous_play"
|
||||||
|
private const val LAST_INSTANT_MIX = "last_instant_mix"
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
@ -476,4 +478,21 @@ object Preferences {
|
||||||
fun setTempoUpdateReminder() {
|
fun setTempoUpdateReminder() {
|
||||||
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
|
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isContinuousPlayEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(CONTINUOUS_PLAY, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setLastInstantMix() {
|
||||||
|
App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isInstantMixUsable(): Boolean {
|
||||||
|
return App.getInstance().preferences.getLong(
|
||||||
|
LAST_INSTANT_MIX, 0
|
||||||
|
) + 5000 < System.currentTimeMillis()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +203,7 @@ public class HomeViewModel extends AndroidViewModel {
|
||||||
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
||||||
mediaInstantMix.setValue(Collections.emptyList());
|
mediaInstantMix.setValue(Collections.emptyList());
|
||||||
|
|
||||||
songRepository.getInstantMix(media, 20).observe(owner, mediaInstantMix::postValue);
|
songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
|
||||||
|
|
||||||
return mediaInstantMix;
|
return mediaInstantMix;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
||||||
instantMix.setValue(Collections.emptyList());
|
instantMix.setValue(Collections.emptyList());
|
||||||
|
|
||||||
songRepository.getInstantMix(media, 20).observe(owner, instantMix::postValue);
|
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
|
||||||
|
|
||||||
return instantMix;
|
return instantMix;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
|
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
|
||||||
instantMix.setValue(Collections.emptyList());
|
instantMix.setValue(Collections.emptyList());
|
||||||
|
|
||||||
songRepository.getInstantMix(media, 20).observe(owner, instantMix::postValue);
|
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
|
||||||
|
|
||||||
return instantMix;
|
return instantMix;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,8 @@
|
||||||
<string name="settings_audio_transcode_priority_toast">Priority on transcoding of track given to server</string>
|
<string name="settings_audio_transcode_priority_toast">Priority on transcoding of track given to server</string>
|
||||||
<string name="settings_buffering_strategy">Buffering strategy</string>
|
<string name="settings_buffering_strategy">Buffering strategy</string>
|
||||||
<string name="settings_buffering_strategy_summary">For the change to take effect you must manually restart the app.</string>
|
<string name="settings_buffering_strategy_summary">For the change to take effect you must manually restart the app.</string>
|
||||||
|
<string name="settings_continuous_play_summary">Allows music to keep playing after a playlist has ended, playing similar songs</string>
|
||||||
|
<string name="settings_continuous_play_title">Continuous play</string>
|
||||||
<string name="settings_covers_cache">Size of artwork cache</string>
|
<string name="settings_covers_cache">Size of artwork cache</string>
|
||||||
<string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers.</string>
|
<string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers.</string>
|
||||||
<string name="settings_data_saving_mode_title">Limit mobile data usage</string>
|
<string name="settings_data_saving_mode_title">Limit mobile data usage</string>
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,12 @@
|
||||||
app:title="@string/settings_image_size"
|
app:title="@string/settings_image_size"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:title="@string/settings_continuous_play_title"
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:summary="@string/settings_continuous_play_summary"
|
||||||
|
android:key="continuous_play" />
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:title="@string/settings_wifi_only_title"
|
android:title="@string/settings_wifi_only_title"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import com.google.android.gms.common.GoogleApiAvailability
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
private lateinit var automotiveRepository: AutomotiveRepository
|
private lateinit var automotiveRepository: AutomotiveRepository
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
private lateinit var castPlayer: CastPlayer
|
private lateinit var castPlayer: CastPlayer
|
||||||
|
|
@ -45,8 +44,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
initializePlayerListener()
|
initializePlayerListener()
|
||||||
|
|
||||||
setPlayer(
|
setPlayer(
|
||||||
null,
|
null,
|
||||||
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player
|
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,18 +72,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
private fun initializePlayer() {
|
private fun initializePlayer() {
|
||||||
player = ExoPlayer.Builder(this)
|
player = ExoPlayer.Builder(this)
|
||||||
.setRenderersFactory(getRenderersFactory())
|
.setRenderersFactory(getRenderersFactory())
|
||||||
.setMediaSourceFactory(getMediaSourceFactory())
|
.setMediaSourceFactory(getMediaSourceFactory())
|
||||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||||
.setHandleAudioBecomingNoisy(true)
|
.setHandleAudioBecomingNoisy(true)
|
||||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||||
.setLoadControl(initializeLoadControl())
|
.setLoadControl(initializeLoadControl())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeCastPlayer() {
|
private fun initializeCastPlayer() {
|
||||||
if (GoogleApiAvailability.getInstance()
|
if (GoogleApiAvailability.getInstance()
|
||||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||||
) {
|
) {
|
||||||
castPlayer = CastPlayer(CastContext.getSharedInstance(this))
|
castPlayer = CastPlayer(CastContext.getSharedInstance(this))
|
||||||
castPlayer.setSessionAvailabilityListener(this)
|
castPlayer.setSessionAvailabilityListener(this)
|
||||||
|
|
@ -93,15 +92,15 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
private fun initializeMediaLibrarySession() {
|
private fun initializeMediaLibrarySession() {
|
||||||
val sessionActivityPendingIntent =
|
val sessionActivityPendingIntent =
|
||||||
TaskStackBuilder.create(this).run {
|
TaskStackBuilder.create(this).run {
|
||||||
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
|
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
|
||||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaLibrarySession =
|
mediaLibrarySession =
|
||||||
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
|
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
|
||||||
.setSessionActivity(sessionActivityPendingIntent)
|
.setSessionActivity(sessionActivityPendingIntent)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
|
private fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
|
||||||
|
|
@ -121,13 +120,16 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
ReplayGainUtil.setReplayGain(player, tracks)
|
ReplayGainUtil.setReplayGain(player, tracks)
|
||||||
MediaManager.scrobble(player.currentMediaItem, false)
|
MediaManager.scrobble(player.currentMediaItem, false)
|
||||||
|
|
||||||
|
if (player.currentMediaItemIndex + 1 == player.mediaItemCount)
|
||||||
|
MediaManager.continuousPlay(player.currentMediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
MediaManager.setPlayingPausedTimestamp(
|
MediaManager.setPlayingPausedTimestamp(
|
||||||
player.currentMediaItem,
|
player.currentMediaItem,
|
||||||
player.currentPosition
|
player.currentPosition
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MediaManager.scrobble(player.currentMediaItem, false)
|
MediaManager.scrobble(player.currentMediaItem, false)
|
||||||
|
|
@ -138,8 +140,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
super.onPlaybackStateChanged(playbackState)
|
super.onPlaybackStateChanged(playbackState)
|
||||||
|
|
||||||
if (!player.hasNextMediaItem() &&
|
if (!player.hasNextMediaItem() &&
|
||||||
playbackState == Player.STATE_ENDED &&
|
playbackState == Player.STATE_ENDED &&
|
||||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||||
) {
|
) {
|
||||||
MediaManager.scrobble(player.currentMediaItem, true)
|
MediaManager.scrobble(player.currentMediaItem, true)
|
||||||
MediaManager.saveChronology(player.currentMediaItem)
|
MediaManager.saveChronology(player.currentMediaItem)
|
||||||
|
|
@ -147,9 +149,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
oldPosition: Player.PositionInfo,
|
oldPosition: Player.PositionInfo,
|
||||||
newPosition: Player.PositionInfo,
|
newPosition: Player.PositionInfo,
|
||||||
reason: Int
|
reason: Int
|
||||||
) {
|
) {
|
||||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||||
|
|
||||||
|
|
@ -169,13 +171,13 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
private fun initializeLoadControl(): DefaultLoadControl {
|
private fun initializeLoadControl(): DefaultLoadControl {
|
||||||
return DefaultLoadControl.Builder()
|
return DefaultLoadControl.Builder()
|
||||||
.setBufferDurationsMs(
|
.setBufferDurationsMs(
|
||||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
|
private fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
|
||||||
|
|
@ -196,7 +198,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||||
|
|
||||||
private fun getMediaSourceFactory() =
|
private fun getMediaSourceFactory() =
|
||||||
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
|
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
|
||||||
|
|
||||||
override fun onCastSessionAvailable() {
|
override fun onCastSessionAvailable() {
|
||||||
setPlayer(player, castPlayer)
|
setPlayer(player, castPlayer)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue