Adress the last requested changes + update YoutubeCommentsExtractor mocks
This commit is contained in:
parent
2320aecb7c
commit
7753556e66
22 changed files with 157 additions and 144 deletions
|
@ -78,7 +78,8 @@ public class YoutubeParsingHelper {
|
||||||
private static String[] youtubeMusicKey;
|
private static String[] youtubeMusicKey;
|
||||||
|
|
||||||
private static boolean keyAndVersionExtracted = false;
|
private static boolean keyAndVersionExtracted = false;
|
||||||
private static Boolean areHardcodedClientVersionAndKeyValidValue = null;
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
private static Optional<Boolean> hardcodedClientVersionAndKeyValid = Optional.empty();
|
||||||
|
|
||||||
private static Random numberGenerator = new Random();
|
private static Random numberGenerator = new Random();
|
||||||
|
|
||||||
|
@ -308,10 +309,10 @@ public class YoutubeParsingHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean areHardcodedClientVersionAndKeyValid()
|
public static Optional<Boolean> areHardcodedClientVersionAndKeyValid()
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
if (areHardcodedClientVersionAndKeyValidValue != null) {
|
if (hardcodedClientVersionAndKeyValid.isPresent()) {
|
||||||
return areHardcodedClientVersionAndKeyValidValue;
|
return hardcodedClientVersionAndKeyValid;
|
||||||
}
|
}
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
final byte[] body = JsonWriter.string()
|
final byte[] body = JsonWriter.string()
|
||||||
|
@ -343,12 +344,12 @@ public class YoutubeParsingHelper {
|
||||||
final String responseBody = response.responseBody();
|
final String responseBody = response.responseBody();
|
||||||
final int responseCode = response.responseCode();
|
final int responseCode = response.responseCode();
|
||||||
|
|
||||||
return areHardcodedClientVersionAndKeyValidValue = responseBody.length() > 5000
|
return hardcodedClientVersionAndKeyValid = Optional.of(responseBody.length() > 5000
|
||||||
&& responseCode == 200; // Ensure to have a valid response
|
&& responseCode == 200); // Ensure to have a valid response
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void extractClientVersionAndKey() throws IOException, ExtractionException {
|
private static void extractClientVersionAndKey() throws IOException, ExtractionException {
|
||||||
// Don't extract the client version and the innertube key if it has been already extracted
|
// Don't extract the client version and the InnerTube key if it has been already extracted
|
||||||
if (keyAndVersionExtracted) return;
|
if (keyAndVersionExtracted) return;
|
||||||
// Don't provide a search term in order to have a smaller response
|
// Don't provide a search term in order to have a smaller response
|
||||||
final String url = "https://www.youtube.com/results?search_query=&ucbcb=1";
|
final String url = "https://www.youtube.com/results?search_query=&ucbcb=1";
|
||||||
|
@ -424,7 +425,7 @@ public class YoutubeParsingHelper {
|
||||||
*/
|
*/
|
||||||
public static String getClientVersion() throws IOException, ExtractionException {
|
public static String getClientVersion() throws IOException, ExtractionException {
|
||||||
if (!isNullOrEmpty(clientVersion)) return clientVersion;
|
if (!isNullOrEmpty(clientVersion)) return clientVersion;
|
||||||
if (areHardcodedClientVersionAndKeyValid()) {
|
if (areHardcodedClientVersionAndKeyValid().orElse(false)) {
|
||||||
return clientVersion = HARDCODED_CLIENT_VERSION;
|
return clientVersion = HARDCODED_CLIENT_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,7 +438,7 @@ public class YoutubeParsingHelper {
|
||||||
*/
|
*/
|
||||||
public static String getKey() throws IOException, ExtractionException {
|
public static String getKey() throws IOException, ExtractionException {
|
||||||
if (!isNullOrEmpty(key)) return key;
|
if (!isNullOrEmpty(key)) return key;
|
||||||
if (areHardcodedClientVersionAndKeyValid()) {
|
if (areHardcodedClientVersionAndKeyValid().orElse(false)) {
|
||||||
return key = HARDCODED_KEY;
|
return key = HARDCODED_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,30 +81,31 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
||||||
ExtractionException {
|
ExtractionException {
|
||||||
final String channel_path = super.getId();
|
final String channelPath = super.getId();
|
||||||
final String[] channelInfo = channel_path.split("/");
|
final String[] channelId = channelPath.split("/");
|
||||||
String id = "";
|
String id = "";
|
||||||
// If the url is an URL which is not a /channel URL, we need to use the
|
// If the url is an URL which is not a /channel URL, we need to use the
|
||||||
// navigation/resolve_url endpoint of the youtubei API to get the channel id. Otherwise, we
|
// navigation/resolve_url endpoint of the InnerTube API to get the channel id. Otherwise,
|
||||||
// couldn't get information about the channel associated with this URL, if there is one.
|
// we couldn't get information about the channel associated with this URL, if there is one.
|
||||||
if (!channelInfo[0].equals("channel")) {
|
if (!channelId[0].equals("channel")) {
|
||||||
final byte[] body = JsonWriter.string(prepareJsonBuilder(getExtractorLocalization(),
|
final byte[] body = JsonWriter.string(prepareJsonBuilder(getExtractorLocalization(),
|
||||||
getExtractorContentCountry())
|
getExtractorContentCountry())
|
||||||
.value("url", "https://www.youtube.com/" + channel_path)
|
.value("url", "https://www.youtube.com/" + channelPath)
|
||||||
.done())
|
.done())
|
||||||
.getBytes(UTF_8);
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
final JsonObject jsonResponse = getJsonPostResponse("navigation/resolve_url",
|
final JsonObject jsonResponse = getJsonPostResponse("navigation/resolve_url",
|
||||||
body, getExtractorLocalization());
|
body, getExtractorLocalization());
|
||||||
|
|
||||||
if (jsonResponse.has("error")) {
|
if (!isNullOrEmpty(jsonResponse.getObject("error"))) {
|
||||||
if (jsonResponse.getInt("code") == 404) {
|
final JsonObject errorJsonObject = jsonResponse.getObject("error");
|
||||||
throw new ContentNotAvailableException(
|
final int errorCode = errorJsonObject.getInt("code");
|
||||||
"No channel associated with this user exists");
|
if (errorCode == 404) {
|
||||||
|
throw new ContentNotAvailableException("This channel doesn't exist.");
|
||||||
} else {
|
} else {
|
||||||
throw new ContentNotAvailableException("Got error:\""
|
throw new ContentNotAvailableException("Got error:\""
|
||||||
+ jsonResponse.getString("status") + "\": "
|
+ errorJsonObject.getString("status") + "\": "
|
||||||
+ jsonResponse.getString("message"));
|
+ errorJsonObject.getString("message"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +129,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
redirectedChannelId = browseId;
|
redirectedChannelId = browseId;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
id = channelInfo[1];
|
id = channelId[1];
|
||||||
}
|
}
|
||||||
JsonObject ajaxJson = null;
|
JsonObject ajaxJson = null;
|
||||||
|
|
||||||
|
@ -145,13 +146,14 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
getExtractorLocalization());
|
getExtractorLocalization());
|
||||||
|
|
||||||
if (!isNullOrEmpty(jsonResponse.getObject("error"))) {
|
if (!isNullOrEmpty(jsonResponse.getObject("error"))) {
|
||||||
final int errorCode = jsonResponse.getObject("error").getInt("code");
|
final JsonObject errorJsonObject = jsonResponse.getObject("error");
|
||||||
if (errorCode == 400) {
|
final int errorCode = errorJsonObject.getInt("code");
|
||||||
|
if (errorCode == 404) {
|
||||||
throw new ContentNotAvailableException("This channel doesn't exist.");
|
throw new ContentNotAvailableException("This channel doesn't exist.");
|
||||||
} else {
|
} else {
|
||||||
throw new ContentNotAvailableException("Got error:\""
|
throw new ContentNotAvailableException("Got error:\""
|
||||||
+ jsonResponse.getString("status") + "\": "
|
+ errorJsonObject.getString("status") + "\": "
|
||||||
+ jsonResponse.getString("message"));
|
+ errorJsonObject.getString("message"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,13 +332,13 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
.getArray("contents").getObject(0).getObject("itemSectionRenderer")
|
.getArray("contents").getObject(0).getObject("itemSectionRenderer")
|
||||||
.getArray("contents").getObject(0).getObject("gridRenderer");
|
.getArray("contents").getObject(0).getObject("gridRenderer");
|
||||||
|
|
||||||
final List<String> channelInfo = new ArrayList<>();
|
final List<String> channelIds = new ArrayList<>();
|
||||||
channelInfo.add(getName());
|
channelIds.add(getName());
|
||||||
channelInfo.add(getUrl());
|
channelIds.add(getUrl());
|
||||||
final JsonObject continuation = collectStreamsFrom(collector, gridRenderer
|
final JsonObject continuation = collectStreamsFrom(collector, gridRenderer
|
||||||
.getArray("items"), channelInfo);
|
.getArray("items"), channelIds);
|
||||||
|
|
||||||
nextPage = getNextPageFrom(continuation, channelInfo);
|
nextPage = getNextPageFrom(continuation, channelIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, nextPage);
|
return new InfoItemsPage<>(collector, nextPage);
|
||||||
|
@ -349,7 +351,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> channelInfos = page.getIds();
|
final List<String> channelIds = page.getIds();
|
||||||
|
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
final Map<String, List<String>> headers = new HashMap<>();
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
@ -365,14 +367,14 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
.getObject("appendContinuationItemsAction");
|
.getObject("appendContinuationItemsAction");
|
||||||
|
|
||||||
final JsonObject continuation = collectStreamsFrom(collector, sectionListContinuation
|
final JsonObject continuation = collectStreamsFrom(collector, sectionListContinuation
|
||||||
.getArray("continuationItems"), channelInfos);
|
.getArray("continuationItems"), channelIds);
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, getNextPageFrom(continuation, channelInfos));
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuation, channelIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Page getNextPageFrom(final JsonObject continuations,
|
private Page getNextPageFrom(final JsonObject continuations,
|
||||||
final List<String> channelInfo) throws IOException,
|
final List<String> channelIds) throws IOException,
|
||||||
ExtractionException {
|
ExtractionException {
|
||||||
if (isNullOrEmpty(continuations)) {
|
if (isNullOrEmpty(continuations)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -388,23 +390,24 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||||
.done())
|
.done())
|
||||||
.getBytes(UTF_8);
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
return new Page(YOUTUBEI_V1_URL + "browse?key=" + getKey(), null, channelInfo, null, body);
|
return new Page(YOUTUBEI_V1_URL + "browse?key=" + getKey(), null, channelIds, null, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect streams from an array of items
|
* Collect streams from an array of items
|
||||||
*
|
*
|
||||||
* @param collector the collector where videos will be commited
|
* @param collector the collector where videos will be committed
|
||||||
* @param videos the array to get videos from
|
* @param videos the array to get videos from
|
||||||
|
* @param channelIds the ids of the channel, which are its name and its URL
|
||||||
* @return the continuation object
|
* @return the continuation object
|
||||||
*/
|
*/
|
||||||
private JsonObject collectStreamsFrom(@Nonnull final StreamInfoItemsCollector collector,
|
private JsonObject collectStreamsFrom(@Nonnull final StreamInfoItemsCollector collector,
|
||||||
@Nonnull final JsonArray videos,
|
@Nonnull final JsonArray videos,
|
||||||
@Nonnull final List<String> channelInfo) {
|
@Nonnull final List<String> channelIds) {
|
||||||
collector.reset();
|
collector.reset();
|
||||||
|
|
||||||
final String uploaderName = channelInfo.get(0);
|
final String uploaderName = channelIds.get(0);
|
||||||
final String uploaderUrl = channelInfo.get(1);
|
final String uploaderUrl = channelIds.get(1);
|
||||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
|
|
||||||
JsonObject continuation = null;
|
JsonObject continuation = null;
|
||||||
|
|
|
@ -189,7 +189,7 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
||||||
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException {
|
||||||
final Map<String, List<String>> requestHeaders = new HashMap<>();
|
final Map<String, List<String>> requestHeaders = new HashMap<>();
|
||||||
requestHeaders.put("User-Agent", singletonList(USER_AGENT));
|
requestHeaders.put("User-Agent", singletonList(USER_AGENT));
|
||||||
final Response response = downloader.get(getUrl() + "&ucbcb=1", requestHeaders, getExtractorLocalization());
|
final Response response = downloader.get(getUrl(), requestHeaders, getExtractorLocalization());
|
||||||
responseBody = YoutubeParsingHelper.unescapeDocument(response.responseBody());
|
responseBody = YoutubeParsingHelper.unescapeDocument(response.responseBody());
|
||||||
ytClientVersion = findValue(responseBody, "INNERTUBE_CONTEXT_CLIENT_VERSION\":\"", "\"");
|
ytClientVersion = findValue(responseBody, "INNERTUBE_CONTEXT_CLIENT_VERSION\":\"", "\"");
|
||||||
ytClientName = Parser.matchGroup1(YT_CLIENT_NAME_PATTERN, responseBody);
|
ytClientName = Parser.matchGroup1(YT_CLIENT_NAME_PATTERN, responseBody);
|
||||||
|
|
|
@ -85,7 +85,6 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
||||||
if (isNullOrEmpty(playlistData)) throw new ExtractionException(
|
if (isNullOrEmpty(playlistData)) throw new ExtractionException(
|
||||||
"Could not get playlistData");
|
"Could not get playlistData");
|
||||||
cookieValue = extractCookieValue(COOKIE_NAME, response);
|
cookieValue = extractCookieValue(COOKIE_NAME, response);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -962,7 +962,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (streamingData.has("formats")) {
|
}
|
||||||
|
if (streamingData.has("formats")) {
|
||||||
final JsonArray formats = streamingData.getArray("formats");
|
final JsonArray formats = streamingData.getArray("formats");
|
||||||
if (!isNullOrEmpty(formats)) {
|
if (!isNullOrEmpty(formats)) {
|
||||||
for (final Object format : formats) {
|
for (final Object format : formats) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.youtube.linkHandler;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -26,24 +27,31 @@ public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory
|
||||||
private static final String SEARCH_URL = "https://www.youtube.com/results?search_query=";
|
private static final String SEARCH_URL = "https://www.youtube.com/results?search_query=";
|
||||||
private static final String MUSIC_SEARCH_URL = "https://music.youtube.com/search?q=";
|
private static final String MUSIC_SEARCH_URL = "https://music.youtube.com/search?q=";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
public static YoutubeSearchQueryHandlerFactory getInstance() {
|
public static YoutubeSearchQueryHandlerFactory getInstance() {
|
||||||
return new YoutubeSearchQueryHandlerFactory();
|
return new YoutubeSearchQueryHandlerFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl(String searchString, List<String> contentFilters, String sortFilter) throws ParsingException {
|
public String getUrl(final String searchString,
|
||||||
|
@Nonnull final List<String> contentFilters,
|
||||||
|
final String sortFilter) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
if (!contentFilters.isEmpty()) {
|
if (!contentFilters.isEmpty()) {
|
||||||
switch (contentFilters.get(0)) {
|
final String contentFilter = contentFilters.get(0);
|
||||||
|
switch (contentFilter) {
|
||||||
case ALL:
|
case ALL:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
case VIDEOS:
|
case VIDEOS:
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8) + "&sp=EgIQAQ%253D%253D";
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8)
|
||||||
|
+ "&sp=EgIQAQ%253D%253D";
|
||||||
case CHANNELS:
|
case CHANNELS:
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8) + "&sp=EgIQAg%253D%253D";
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8)
|
||||||
|
+ "&sp=EgIQAg%253D%253D";
|
||||||
case PLAYLISTS:
|
case PLAYLISTS:
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8) + "&sp=EgIQAw%253D%253D";
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8)
|
||||||
|
+ "&sp=EgIQAw%253D%253D";
|
||||||
case MUSIC_SONGS:
|
case MUSIC_SONGS:
|
||||||
case MUSIC_VIDEOS:
|
case MUSIC_VIDEOS:
|
||||||
case MUSIC_ALBUMS:
|
case MUSIC_ALBUMS:
|
||||||
|
@ -54,7 +62,7 @@ public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8);
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8);
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (final UnsupportedEncodingException e) {
|
||||||
throw new ParsingException("Could not encode query", e);
|
throw new ParsingException("Could not encode query", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +82,7 @@ public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
public static String getSearchParameter(final String contentFilter) {
|
public static String getSearchParameter(final String contentFilter) {
|
||||||
if (isNullOrEmpty(contentFilter)) return "";
|
if (isNullOrEmpty(contentFilter)) return "";
|
||||||
switch (contentFilter) {
|
switch (contentFilter) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class YoutubeParsingHelperTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAreHardcodedClientVersionAndKeyValid() throws IOException, ExtractionException {
|
public void testAreHardcodedClientVersionAndKeyValid() throws IOException, ExtractionException {
|
||||||
assertTrue("Hardcoded client version and key are not valid anymore",
|
assertTrue("Hardcoded client version and key are not valid anymore",
|
||||||
YoutubeParsingHelper.areHardcodedClientVersionAndKeyValid());
|
YoutubeParsingHelper.areHardcodedClientVersionAndKeyValid().orElse(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -44,7 +44,7 @@ public class YoutubeParsingHelperTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConvertFromGoogleCacheUrl() throws ParsingException {
|
public void testConvertFromGoogleCacheUrl() {
|
||||||
assertEquals("https://mohfw.gov.in/",
|
assertEquals("https://mohfw.gov.in/",
|
||||||
YoutubeParsingHelper.extractCachedUrlIfNeeded("https://webcache.googleusercontent.com/search?q=cache:https://mohfw.gov.in/"));
|
YoutubeParsingHelper.extractCachedUrlIfNeeded("https://webcache.googleusercontent.com/search?q=cache:https://mohfw.gov.in/"));
|
||||||
assertEquals("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html",
|
assertEquals("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html",
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue