From 78880747f4fdeebc35ebb274f8599db26ed66373 Mon Sep 17 00:00:00 2001 From: Momo the Bestest <45446348+Svagtlys@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:14:01 +0000 Subject: [PATCH 1/4] feat: Add components to Subsonic API implementation to allow for jukebox control --- .../tempo/subsonic/Subsonic.java | 9 +++++ .../jukeboxcontrol/JukeboxControlClient.java | 37 +++++++++++++++++++ .../jukeboxcontrol/JukeboxControlService.java | 16 ++++++++ 3 files changed, 62 insertions(+) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlClient.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/subsonic/api/jukeboxcontrol/JukeboxControlService.java 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 ); +} From a0799963a4b461ef8ec0a791a3c2710bff1c5f57 Mon Sep 17 00:00:00 2001 From: Momo the Bestest <45446348+Svagtlys@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:14:24 +0000 Subject: [PATCH 2/4] feat: Update MediaManager to allow playing media to jukebox --- .../tempo/service/MediaManager.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) 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 f7cd8a381..19aa236cf 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -32,9 +32,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; public class MediaManager { @@ -223,6 +230,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(() -> { @@ -316,6 +470,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(() -> { From e056855b438dddd03735558cb8b476077205e986 Mon Sep 17 00:00:00 2001 From: Momo the Bestest <45446348+Svagtlys@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:15:29 +0000 Subject: [PATCH 3/4] feat: Add UI elements to album and playlist view pages to play on jukebox --- .../tempo/ui/fragment/AlbumPageFragment.java | 5 +++++ .../tempo/ui/fragment/PlaylistPageFragment.java | 4 ++++ app/src/main/res/layout/fragment_album_page.xml | 14 ++++++++++++++ app/src/main/res/layout/fragment_playlist_page.xml | 14 ++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 39 insertions(+) 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/res/layout/fragment_album_page.xml b/app/src/main/res/layout/fragment_album_page.xml index 411d5c1cf..3fd6e2c6d 100644 --- a/app/src/main/res/layout/fragment_album_page.xml +++ b/app/src/main/res/layout/fragment_album_page.xml @@ -213,6 +213,20 @@ app:iconGravity="textStart" app:iconPadding="18dp" /> +