Skip to content

Commit 8ab67bc

Browse files
committed
feat(YouTube Music): Add Disable music video in album patch inotia00/ReVanced_Extended#2568
1 parent 1da2664 commit 8ab67bc

File tree

12 files changed

+549
-0
lines changed

12 files changed

+549
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package app.revanced.extension.music.patches.misc;
2+
3+
import androidx.annotation.NonNull;
4+
5+
import java.util.concurrent.atomic.AtomicBoolean;
6+
7+
import app.revanced.extension.music.patches.misc.requests.PipedRequester;
8+
import app.revanced.extension.music.settings.Settings;
9+
import app.revanced.extension.music.utils.VideoUtils;
10+
import app.revanced.extension.shared.utils.Logger;
11+
12+
@SuppressWarnings("unused")
13+
public class AlbumMusicVideoPatch {
14+
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
15+
private static final boolean DISABLE_MUSIC_VIDEO_IN_ALBUM =
16+
Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM.get();
17+
18+
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
19+
20+
@NonNull
21+
private static volatile String playerResponseVideoId = "";
22+
23+
@NonNull
24+
private static volatile String currentVideoId = "";
25+
26+
/**
27+
* Injection point.
28+
*/
29+
public static void newPlayerResponse(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
30+
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
31+
return;
32+
}
33+
if (!playlistId.startsWith(YOUTUBE_MUSIC_ALBUM_PREFIX)) {
34+
return;
35+
}
36+
if (playlistIndex < 0) {
37+
return;
38+
}
39+
if (playerResponseVideoId.equals(videoId)) {
40+
return;
41+
}
42+
playerResponseVideoId = videoId;
43+
44+
// Fetch from piped instances.
45+
PipedRequester.fetchRequestIfNeeded(videoId, playlistId, playlistIndex);
46+
}
47+
48+
/**
49+
* Injection point.
50+
*/
51+
public static void newVideoLoaded(@NonNull String videoId) {
52+
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
53+
return;
54+
}
55+
if (currentVideoId.equals(videoId)) {
56+
return;
57+
}
58+
currentVideoId = videoId;
59+
60+
// If the user is using a not fast enough internet connection, there will be a slight delay.
61+
// Otherwise, the video may open repeatedly.
62+
VideoUtils.runOnMainThreadDelayed(() -> openOfficialMusicIfNeeded(videoId), 750);
63+
}
64+
65+
private static void openOfficialMusicIfNeeded(@NonNull String videoId) {
66+
try {
67+
PipedRequester request = PipedRequester.getRequestForVideoId(videoId);
68+
if (request == null) {
69+
return;
70+
}
71+
String songId = request.getStream();
72+
if (songId == null) {
73+
return;
74+
}
75+
76+
// It is handled by YouTube Music's internal code.
77+
// There is a slight delay before the dismiss request is reflected.
78+
VideoUtils.dismissQueue();
79+
80+
// Every time a new video is opened, a snack bar appears indicating that the account has been switched.
81+
// To prevent this, hide the snack bar while a new video is opening.
82+
isVideoLaunched.compareAndSet(false, true);
83+
84+
// The newly opened video is not a music video.
85+
// To prevent fetch requests from being sent, set the video id to the newly opened video
86+
VideoUtils.runOnMainThreadDelayed(() -> {
87+
playerResponseVideoId = songId;
88+
currentVideoId = songId;
89+
VideoUtils.openInYouTubeMusic(songId);
90+
}, 750);
91+
92+
// If a new video is opened, the snack bar will be shown.
93+
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 1500);
94+
} catch (Exception ex) {
95+
Logger.printException(() -> "openOfficialMusicIfNeeded failure", ex);
96+
}
97+
}
98+
99+
/**
100+
* Injection point.
101+
*/
102+
public static boolean hideSnackBar() {
103+
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
104+
return false;
105+
}
106+
return isVideoLaunched.get();
107+
}
108+
109+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package app.revanced.extension.music.patches.misc.requests;
2+
3+
import android.annotation.SuppressLint;
4+
5+
import androidx.annotation.GuardedBy;
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
9+
import org.json.JSONException;
10+
import org.json.JSONObject;
11+
12+
import java.io.IOException;
13+
import java.net.HttpURLConnection;
14+
import java.net.SocketTimeoutException;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
import java.util.concurrent.ExecutionException;
18+
import java.util.concurrent.Future;
19+
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.TimeoutException;
21+
22+
import app.revanced.extension.shared.requests.Requester;
23+
import app.revanced.extension.shared.utils.Logger;
24+
import app.revanced.extension.shared.utils.Utils;
25+
26+
public class PipedRequester {
27+
/**
28+
* How long to keep fetches until they are expired.
29+
*/
30+
private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute
31+
32+
private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds
33+
34+
@GuardedBy("itself")
35+
private static final Map<String, PipedRequester> cache = new HashMap<>();
36+
37+
@SuppressLint("ObsoleteSdkInt")
38+
public static void fetchRequestIfNeeded(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
39+
synchronized (cache) {
40+
final long now = System.currentTimeMillis();
41+
42+
cache.values().removeIf(request -> {
43+
final boolean expired = request.isExpired(now);
44+
if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
45+
return expired;
46+
});
47+
48+
if (!cache.containsKey(videoId)) {
49+
PipedRequester pipedRequester = new PipedRequester(videoId, playlistId, playlistIndex);
50+
cache.put(videoId, pipedRequester);
51+
}
52+
}
53+
}
54+
55+
@Nullable
56+
public static PipedRequester getRequestForVideoId(@Nullable String videoId) {
57+
synchronized (cache) {
58+
return cache.get(videoId);
59+
}
60+
}
61+
62+
/**
63+
* TCP timeout
64+
*/
65+
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 2 * 1000; // 2 seconds
66+
67+
/**
68+
* HTTP response timeout
69+
*/
70+
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 4 * 1000; // 4 seconds
71+
72+
@Nullable
73+
private static JSONObject send(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
74+
final long startTime = System.currentTimeMillis();
75+
Logger.printDebug(() -> "Fetching piped instances (videoId: '" + videoId +
76+
"', playlistId: '" + playlistId + "', playlistIndex: '" + playlistIndex + "'");
77+
78+
try {
79+
HttpURLConnection connection = PipedRoutes.getPlaylistConnectionFromRoute(playlistId);
80+
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
81+
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
82+
83+
final int responseCode = connection.getResponseCode();
84+
if (responseCode == 200) return Requester.parseJSONObject(connection);
85+
86+
handleConnectionError("API not available: " + responseCode);
87+
} catch (SocketTimeoutException ex) {
88+
handleConnectionError("Connection timeout", ex);
89+
} catch (IOException ex) {
90+
handleConnectionError("Network error", ex);
91+
} catch (Exception ex) {
92+
Logger.printException(() -> "send failed", ex);
93+
} finally {
94+
Logger.printDebug(() -> "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
95+
}
96+
97+
return null;
98+
}
99+
100+
@Nullable
101+
private static String fetch(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
102+
final JSONObject playlistJson = send(videoId, playlistId, playlistIndex);
103+
if (playlistJson != null) {
104+
try {
105+
final String songId = playlistJson.getJSONArray("relatedStreams")
106+
.getJSONObject(playlistIndex)
107+
.getString("url")
108+
.replaceAll("/.+=", "");
109+
if (songId.isEmpty()) {
110+
handleConnectionError("Url is empty!");
111+
} else if (!songId.equals(videoId)) {
112+
return songId;
113+
}
114+
} catch (JSONException e) {
115+
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
116+
}
117+
}
118+
119+
return null;
120+
}
121+
122+
private static void handleConnectionError(@NonNull String errorMessage) {
123+
handleConnectionError(errorMessage, null);
124+
}
125+
126+
private static void handleConnectionError(@NonNull String errorMessage, @Nullable Exception ex) {
127+
if (ex != null) {
128+
Logger.printInfo(() -> errorMessage, ex);
129+
}
130+
}
131+
132+
133+
/**
134+
* Time this instance and the fetch future was created.
135+
*/
136+
private final long timeFetched;
137+
private final String videoId;
138+
private final Future<String> future;
139+
140+
private PipedRequester(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
141+
this.timeFetched = System.currentTimeMillis();
142+
this.videoId = videoId;
143+
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playlistId, playlistIndex));
144+
}
145+
146+
public boolean isExpired(long now) {
147+
final long timeSinceCreation = now - timeFetched;
148+
if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
149+
return true;
150+
}
151+
152+
// Only expired if the fetch failed (API null response).
153+
return (fetchCompleted() && getStream() == null);
154+
}
155+
156+
/**
157+
* @return if the fetch call has completed.
158+
*/
159+
public boolean fetchCompleted() {
160+
return future.isDone();
161+
}
162+
163+
public String getStream() {
164+
try {
165+
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
166+
} catch (TimeoutException ex) {
167+
Logger.printInfo(() -> "getStream timed out", ex);
168+
} catch (InterruptedException ex) {
169+
Logger.printException(() -> "getStream interrupted", ex);
170+
Thread.currentThread().interrupt(); // Restore interrupt status flag.
171+
} catch (ExecutionException ex) {
172+
Logger.printException(() -> "getStream failure", ex);
173+
}
174+
175+
return null;
176+
}
177+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package app.revanced.extension.music.patches.misc.requests;
2+
3+
import static app.revanced.extension.shared.requests.Route.Method.GET;
4+
5+
import java.io.IOException;
6+
import java.net.HttpURLConnection;
7+
8+
import app.revanced.extension.shared.requests.Requester;
9+
import app.revanced.extension.shared.requests.Route;
10+
11+
class PipedRoutes {
12+
private static final String PIPED_URL = "https://pipedapi.kavin.rocks/";
13+
private static final Route GET_PLAYLIST = new Route(GET, "playlists/{playlist_id}");
14+
15+
private PipedRoutes() {
16+
}
17+
18+
static HttpURLConnection getPlaylistConnectionFromRoute(String... params) throws IOException {
19+
return Requester.getConnectionFromRoute(PIPED_URL, GET_PLAYLIST, params);
20+
}
21+
22+
}

extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ public class Settings extends BaseSettings {
178178
public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true);
179179
public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true);
180180
public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true);
181+
public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true);
181182
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
182183
public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false);
183184
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);

extensions/shared/src/main/java/app/revanced/extension/music/utils/VideoUtils.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ public static void openInYouTubeMusic(@NonNull String songId) {
7171
launchView(url, context.getPackageName());
7272
}
7373

74+
/**
75+
* Rest of the implementation added by patch.
76+
*/
77+
public static void dismissQueue() {
78+
Log.d("Extended: VideoUtils", "Queue dismissed");
79+
}
80+
7481
/**
7582
* Rest of the implementation added by patch.
7683
*/

0 commit comments

Comments
 (0)