diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index 02cbd2399..df3673734 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -33,9 +33,16 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; import java.lang.ref.WeakReference; import java.util.List; +import java.util.Collections; +import java.util.stream.Collectors; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -239,6 +246,153 @@ public static void startQueue(ListenableFuture mediaBrowserListena } } + public static void startQueue(List media, int startIndex) { + + + //Stop current media + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("stop", null, null, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + //Pull the ids from the list of media and put into a string list for jukeboxControl + List ids = media.stream() + .map(Child::getId) + .collect(Collectors.toList()); + + int itemCount = media.size(); + + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("set", null, null, ids, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + // Then skip to startIndex if valid + if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("skip", startIndex, null, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + // set does NOT auto-start the queue, must start it here + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("start", null, null, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + else { + + // set does NOT auto-start the queue, must start it here + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("start", null, null, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + } + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + } + + public static void startQueue(Child media) { + //Stop current media + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("stop", null, null, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List ids = Collections.singletonList(media.getId()); + + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("set", null, null, ids, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + // set does NOT auto-start the queue, must start it here + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("start", null, null, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + + } + public static void playDownloadedMediaItem(ListenableFuture mediaBrowserListenableFuture, MediaItem mediaItem) { if (mediaBrowserListenableFuture != null && mediaItem != null) { mediaBrowserListenableFuture.addListener(() -> { @@ -338,6 +492,47 @@ public static void enqueue(ListenableFuture mediaBrowserListenable } } + public static void enqueue(List media) { + List ids = media.stream() + .map(Child::getId) + .collect(Collectors.toList()); + + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("add", null, null, ids, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public static void enqueue(Child media){ + + List ids = Collections.singletonList(media.getId()); + + App.getSubsonicClientInstance(false) + .getJukeboxControlClient() + .jukeboxControl("add", null, null, ids, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + public static void shuffle(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex, int endIndex) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java index de4b36b78..801ee3457 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java @@ -4,6 +4,7 @@ import com.cappielloantonio.tempo.subsonic.api.bookmarks.BookmarksClient; import com.cappielloantonio.tempo.subsonic.api.browsing.BrowsingClient; import com.cappielloantonio.tempo.subsonic.api.internetradio.InternetRadioClient; +import com.cappielloantonio.tempo.subsonic.api.jukeboxcontrol.JukeboxControlClient; import com.cappielloantonio.tempo.subsonic.api.mediaannotation.MediaAnnotationClient; import com.cappielloantonio.tempo.subsonic.api.medialibraryscanning.MediaLibraryScanningClient; import com.cappielloantonio.tempo.subsonic.api.mediaretrieval.MediaRetrievalClient; @@ -35,6 +36,7 @@ public class Subsonic { private MediaLibraryScanningClient mediaLibraryScanningClient; private BookmarksClient bookmarksClient; private InternetRadioClient internetRadioClient; + private JukeboxControlClient jukeboxControlClient; private SharingClient sharingClient; private OpenClient openClient; @@ -123,6 +125,13 @@ public InternetRadioClient getInternetRadioClient() { return internetRadioClient; } + public JukeboxControlClient getJukeboxControlClient() { + if (jukeboxControlClient == null) { + jukeboxControlClient = new JukeboxControlClient(this); + } + return jukeboxControlClient; + } + public SharingClient getSharingClient() { if (sharingClient == null) { sharingClient = new SharingClient(this); diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlClient.java new file mode 100644 index 000000000..81427d3ff --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlClient.java @@ -0,0 +1,37 @@ +package com.cappielloantonio.tempo.subsonic.api.jukeboxcontrol; + +import android.util.Log; +import java.util.List; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class JukeboxControlClient { + private static final String TAG = "JukeboxControlClient"; + + private final Subsonic subsonic; + private final JukeboxControlService jukeboxControlService; + + public JukeboxControlClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.jukeboxControlService = new RetrofitClient(subsonic).getRetrofit().create(JukeboxControlService.class); + } + + // see https://opensubsonic.netlify.app/docs/endpoints/jukeboxcontrol/ for actions + // "set" to clear queue and add id(s) to queue (does not stop currently playing track) + // "add" to add to end of queue + + // index is only used by actions skip and remove + // offset is only used by action skip + // id is only used by actions add and set + // gain is only used by action setGain + + public Call jukeboxControl(String action, Integer index, Integer offset, List ids, Float gain) { + Log.d(TAG, "jukeboxControl()"); + return jukeboxControlService.jukeboxControl(subsonic.getParams(), action, index, offset, ids, gain); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlService.java new file mode 100644 index 000000000..f302ba29d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlService.java @@ -0,0 +1,16 @@ +package com.cappielloantonio.tempo.subsonic.api.jukeboxcontrol; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface JukeboxControlService { + @GET("jukeboxControl") + Call jukeboxControl(@QueryMap Map params, @Query("action") String action, @Query("index") Integer index, @Query("offset") Integer offset, @Query("id") List ids, @Query("gain") Float gain ); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java index 9bf958038..2a1030a12 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java @@ -298,10 +298,15 @@ private void initMusicButton() { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); }); + + bind.albumPageJukeboxButton.setOnClickListener(v -> { + MediaManager.startQueue(songs, 0); + }); } if (bind != null && songs.isEmpty()) { bind.albumPagePlayButton.setEnabled(false); + bind.albumPageJukeboxButton.setEnabled(false); bind.albumPageShuffleButton.setEnabled(false); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java index d4cf6c0fc..d22de45cb 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java @@ -220,6 +220,10 @@ private void initMusicButton() { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); }); + + bind.playlistPageJukeboxButton.setOnClickListener(v -> { + MediaManager.startQueue(songs, 0); + }); } }); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index a6167eed9..8f125bd81 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -170,6 +170,11 @@ public void onLoadMedia(List media) { dismissBottomSheet(); })); + TextView addToQueueJukebox = view.findViewById(R.id.add_to_queue_jukebox_text_view); + addToQueueJukebox.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + MediaManager.enqueue(songs); + })); + TextView downloadAll = view.findViewById(R.id.download_all_text_view); albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { List mediaItems = MappingUtil.mapDownloads(songs); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index 39ba43943..51a80b96e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -175,6 +175,11 @@ private void init(View view) { dismissBottomSheet(); }); + TextView addToQueueJukebox = view.findViewById(R.id.add_to_queue_jukebox_text_view); + addToQueueJukebox.setOnClickListener(v -> { + MediaManager.enqueue(song); + }); + TextView rate = view.findViewById(R.id.rate_text_view); rate.setOnClickListener(v -> { Bundle bundle = new Bundle(); diff --git a/app/src/main/res/layout/bottom_sheet_album_dialog.xml b/app/src/main/res/layout/bottom_sheet_album_dialog.xml index b37a5f90c..7e500ee6f 100644 --- a/app/src/main/res/layout/bottom_sheet_album_dialog.xml +++ b/app/src/main/res/layout/bottom_sheet_album_dialog.xml @@ -133,6 +133,19 @@ android:paddingBottom="12dp" android:text="@string/album_bottom_sheet_add_to_queue" /> + + + + +