mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 21:27:43 +02:00
fix(YouTube Music - Disable music video in album): Piped API not available
This commit is contained in:
@ -9,7 +9,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import app.revanced.extension.music.patches.misc.requests.PipedRequester;
|
||||
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
import app.revanced.extension.music.shared.VideoInformation;
|
||||
import app.revanced.extension.music.utils.VideoUtils;
|
||||
@ -76,8 +76,8 @@ public class AlbumMusicVideoPatch {
|
||||
}
|
||||
playerResponseVideoId = videoId;
|
||||
|
||||
// Fetch Piped instance.
|
||||
PipedRequester.fetchRequestIfNeeded(videoId, playlistId, playlistIndex);
|
||||
// Fetch.
|
||||
PlaylistRequest.fetchRequestIfNeeded(videoId, playlistId, playlistIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,7 +96,7 @@ public class AlbumMusicVideoPatch {
|
||||
|
||||
private static void checkVideo(@NonNull String videoId) {
|
||||
try {
|
||||
PipedRequester request = PipedRequester.getRequestForVideoId(videoId);
|
||||
PlaylistRequest request = PlaylistRequest.getRequestForVideoId(videoId);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
@ -109,7 +109,7 @@ public class AlbumMusicVideoPatch {
|
||||
Logger.printException(() -> "Error: Blocking main thread");
|
||||
}
|
||||
String songId = request.getStream();
|
||||
if (songId == null) {
|
||||
if (songId.isEmpty()) {
|
||||
Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
|
||||
return;
|
||||
}
|
||||
@ -157,7 +157,7 @@ public class AlbumMusicVideoPatch {
|
||||
playerResponseVideoId = songId;
|
||||
currentVideoId = songId;
|
||||
VideoUtils.openInYouTubeMusic(songId);
|
||||
}, 750);
|
||||
}, 1000);
|
||||
|
||||
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 1500);
|
||||
} catch (Exception ex) {
|
||||
|
@ -1,159 +0,0 @@
|
||||
package app.revanced.extension.music.patches.misc.requests;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
|
||||
public class PipedRequester {
|
||||
private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 4 * 1000; // 4 seconds
|
||||
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, PipedRequester> cache = new LinkedHashMap<>() {
|
||||
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 10;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
public static void fetchRequestIfNeeded(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
|
||||
synchronized (cache) {
|
||||
if (!cache.containsKey(videoId)) {
|
||||
PipedRequester pipedRequester = new PipedRequester(videoId, playlistId, playlistIndex);
|
||||
cache.put(videoId, pipedRequester);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PipedRequester getRequestForVideoId(@Nullable String videoId) {
|
||||
synchronized (cache) {
|
||||
return cache.get(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP timeout
|
||||
*/
|
||||
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 2 * 1000; // 2 seconds
|
||||
|
||||
/**
|
||||
* HTTP response timeout
|
||||
*/
|
||||
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 4 * 1000; // 4 seconds
|
||||
|
||||
@Nullable
|
||||
private static JSONObject send(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
Logger.printDebug(() -> "Fetching piped instances (videoId: '" + videoId +
|
||||
"', playlistId: '" + playlistId + "', playlistIndex: '" + playlistIndex + "')");
|
||||
|
||||
try {
|
||||
HttpURLConnection connection = PipedRoutes.getPlaylistConnectionFromRoute(playlistId);
|
||||
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
|
||||
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
||||
|
||||
handleConnectionError("API not available: " + responseCode);
|
||||
} catch (SocketTimeoutException ex) {
|
||||
handleConnectionError("Connection timeout", ex);
|
||||
} catch (IOException ex) {
|
||||
handleConnectionError("Network error", ex);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "send failed", ex);
|
||||
} finally {
|
||||
Logger.printDebug(() -> "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String fetch(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
|
||||
final JSONObject playlistJson = send(videoId, playlistId, playlistIndex);
|
||||
if (playlistJson != null) {
|
||||
try {
|
||||
final String songId = playlistJson.getJSONArray("relatedStreams")
|
||||
.getJSONObject(playlistIndex)
|
||||
.getString("url")
|
||||
.replaceAll("/.+=", "");
|
||||
if (songId.isEmpty()) {
|
||||
handleConnectionError("Url is empty!");
|
||||
} else if (!songId.equals(videoId)) {
|
||||
Logger.printDebug(() -> "Video found (videoId: '" + videoId +
|
||||
"', songId: '" + songId + "')");
|
||||
return songId;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void handleConnectionError(@NonNull String errorMessage) {
|
||||
handleConnectionError(errorMessage, null);
|
||||
}
|
||||
|
||||
private static void handleConnectionError(@NonNull String errorMessage, @Nullable Exception ex) {
|
||||
if (ex != null) {
|
||||
Logger.printInfo(() -> errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Time this instance and the fetch future was created.
|
||||
*/
|
||||
private final Future<String> future;
|
||||
|
||||
private PipedRequester(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
|
||||
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playlistId, playlistIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the fetch call has completed.
|
||||
*/
|
||||
public boolean fetchCompleted() {
|
||||
return future.isDone();
|
||||
}
|
||||
|
||||
public String getStream() {
|
||||
try {
|
||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
Logger.printInfo(() -> "getStream timed out", ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Logger.printException(() -> "getStream interrupted", ex);
|
||||
Thread.currentThread().interrupt(); // Restore interrupt status flag.
|
||||
} catch (ExecutionException ex) {
|
||||
Logger.printException(() -> "getStream failure", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package app.revanced.extension.music.patches.misc.requests;
|
||||
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
class PipedRoutes {
|
||||
private static final String PIPED_URL = "https://pipedapi.kavin.rocks/";
|
||||
private static final Route GET_PLAYLIST = new Route(GET, "playlists/{playlist_id}");
|
||||
|
||||
private PipedRoutes() {
|
||||
}
|
||||
|
||||
static HttpURLConnection getPlaylistConnectionFromRoute(String... params) throws IOException {
|
||||
return Requester.getConnectionFromRoute(PIPED_URL, GET_PLAYLIST, params);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package app.revanced.extension.music.patches.misc.requests
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.GuardedBy
|
||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
||||
import app.revanced.extension.shared.requests.Requester
|
||||
import app.revanced.extension.shared.utils.Logger
|
||||
import app.revanced.extension.shared.utils.Utils
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
class PlaylistRequest private constructor(
|
||||
private val videoId: String,
|
||||
private val playlistId: String,
|
||||
private val playlistIndex: Int,
|
||||
) {
|
||||
/**
|
||||
* Time this instance and the fetch future was created.
|
||||
*/
|
||||
private val timeFetched = System.currentTimeMillis()
|
||||
private val future: Future<String> = Utils.submitOnBackgroundThread {
|
||||
fetch(
|
||||
videoId,
|
||||
playlistId,
|
||||
playlistIndex,
|
||||
)
|
||||
}
|
||||
|
||||
fun isExpired(now: Long): Boolean {
|
||||
val timeSinceCreation = now - timeFetched
|
||||
if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only expired if the fetch failed (API null response).
|
||||
return (fetchCompleted() && stream.isEmpty())
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the fetch call has completed.
|
||||
*/
|
||||
fun fetchCompleted(): Boolean {
|
||||
return future.isDone
|
||||
}
|
||||
|
||||
val stream: String
|
||||
get() {
|
||||
try {
|
||||
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS]
|
||||
} catch (ex: TimeoutException) {
|
||||
Logger.printInfo(
|
||||
{ "getStream timed out" },
|
||||
ex
|
||||
)
|
||||
} catch (ex: InterruptedException) {
|
||||
Logger.printException(
|
||||
{ "getStream interrupted" },
|
||||
ex
|
||||
)
|
||||
Thread.currentThread().interrupt() // Restore interrupt status flag.
|
||||
} catch (ex: ExecutionException) {
|
||||
Logger.printException(
|
||||
{ "getStream failure" },
|
||||
ex
|
||||
)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* How long to keep fetches until they are expired.
|
||||
*/
|
||||
private const val CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000L // 1 Minute
|
||||
|
||||
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 10 * 1000L // 10 seconds
|
||||
|
||||
@GuardedBy("itself")
|
||||
private val cache: MutableMap<String, PlaylistRequest> = HashMap()
|
||||
|
||||
@JvmStatic
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun fetchRequestIfNeeded(
|
||||
videoId: String,
|
||||
playlistId: String,
|
||||
playlistIndex: Int,
|
||||
) {
|
||||
Objects.requireNonNull(videoId)
|
||||
synchronized(cache) {
|
||||
val now = System.currentTimeMillis()
|
||||
cache.values.removeIf { request: PlaylistRequest ->
|
||||
val expired = request.isExpired(now)
|
||||
if (expired) Logger.printDebug { "Removing expired stream: " + request.videoId }
|
||||
expired
|
||||
}
|
||||
if (!cache.containsKey(videoId)) {
|
||||
cache[videoId] = PlaylistRequest(
|
||||
videoId,
|
||||
playlistId,
|
||||
playlistIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getRequestForVideoId(videoId: String): PlaylistRequest? {
|
||||
synchronized(cache) {
|
||||
return cache[videoId]
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
||||
Logger.printInfo({ toastMessage }, ex)
|
||||
}
|
||||
|
||||
private fun sendRequest(
|
||||
videoId: String,
|
||||
playlistId: String,
|
||||
): JSONObject? {
|
||||
Objects.requireNonNull(videoId)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
val clientType = YouTubeAppClient.ClientType.ANDROID_VR
|
||||
val clientTypeName = clientType.name
|
||||
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
||||
|
||||
try {
|
||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
||||
PlayerRoutes.GET_PLAYLIST_PAGE,
|
||||
clientType
|
||||
)
|
||||
val requestBody =
|
||||
PlayerRoutes.createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId,
|
||||
playlistId = playlistId
|
||||
)
|
||||
|
||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||
connection.outputStream.write(requestBody)
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||
|
||||
handleConnectionError(
|
||||
(clientTypeName + " not available with response code: "
|
||||
+ responseCode + " message: " + connection.responseMessage),
|
||||
null
|
||||
)
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
handleConnectionError("Connection timeout", ex)
|
||||
} catch (ex: IOException) {
|
||||
handleConnectionError("Network error", ex)
|
||||
} catch (ex: Exception) {
|
||||
Logger.printException({ "sendRequest failed" }, ex)
|
||||
} finally {
|
||||
Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseResponse(playlistJson: JSONObject, playlistIndex: Int): String {
|
||||
try {
|
||||
val singleColumnWatchNextResultsJsonObject: JSONObject =
|
||||
playlistJson
|
||||
.getJSONObject("contents")
|
||||
.getJSONObject("singleColumnWatchNextResults")
|
||||
|
||||
if (singleColumnWatchNextResultsJsonObject.has("playlist")) {
|
||||
val playlistJsonObject: JSONObject? =
|
||||
singleColumnWatchNextResultsJsonObject
|
||||
.getJSONObject("playlist")
|
||||
.getJSONObject("playlist")
|
||||
|
||||
val currentStreamJsonObject = playlistJsonObject
|
||||
?.getJSONArray("contents")
|
||||
?.get(playlistIndex)
|
||||
|
||||
if (currentStreamJsonObject is JSONObject) {
|
||||
val watchEndpointJsonObject: JSONObject? =
|
||||
currentStreamJsonObject
|
||||
.getJSONObject("playlistPanelVideoRenderer")
|
||||
.getJSONObject("navigationEndpoint")
|
||||
.getJSONObject("watchEndpoint")
|
||||
|
||||
return watchEndpointJsonObject?.getString("videoId") + ""
|
||||
}
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Logger.printException(
|
||||
{ "Fetch failed while processing response data for response: $playlistJson" },
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun fetch(
|
||||
videoId: String,
|
||||
playlistId: String,
|
||||
playlistIndex: Int,
|
||||
): String {
|
||||
val playlistJson = sendRequest(
|
||||
videoId,
|
||||
playlistId,
|
||||
)
|
||||
if (playlistJson != null) {
|
||||
return parseResponse(playlistJson, playlistIndex)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user