From 7d07924de8ee57358696d11ae842e32c938d5f35 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 9 Jan 2022 22:49:37 +0100 Subject: [PATCH] [YouTube] Try to use lighter requests when extracting client version and key from YouTube and YouTube Music This is done by fetching https://www.youtube.com/sw.js for YouTube and https://music.youtube.com/sw.js for YouTube Music. Two new methods in Utils class have been added which allow to try to get a match of regular expressions in a string array, or a Pattern array, on a content, on a specific index or 0. Also some code refactoring has been made in this class. --- .../youtube/YoutubeParsingHelper.java | 145 ++++++++------- .../newpipe/extractor/utils/Parser.java | 63 ++++--- .../schabi/newpipe/extractor/utils/Utils.java | 171 +++++++++++++++--- 3 files changed, 264 insertions(+), 115 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 88da1cacf..12a992e16 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -5,6 +5,7 @@ import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; import static org.schabi.newpipe.extractor.utils.Utils.HTTP; import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; import static org.schabi.newpipe.extractor.utils.Utils.UTF_8; +import static org.schabi.newpipe.extractor.utils.Utils.getStringResultFromRegexArray; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import com.grack.nanojson.JsonArray; @@ -57,20 +58,20 @@ import javax.annotation.Nullable; * Created by Christian Schabesberger on 02.03.16. * * Copyright (C) Christian Schabesberger 2016 - * YoutubeParsingHelper.java is part of NewPipe. + * YoutubeParsingHelper.java is part of NewPipe Extractor. * - * NewPipe is free software: you can redistribute it and/or modify + * NewPipe Extractor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * NewPipe is distributed in the hope that it will be useful, + * NewPipe Extractor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . + * along with NewPipe Extractor. If not, see . */ public final class YoutubeParsingHelper { @@ -98,6 +99,15 @@ public final class YoutubeParsingHelper { private static boolean keyAndVersionExtracted = false; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private static Optional hardcodedClientVersionAndKeyValid = Optional.empty(); + private static final String[] INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES = + {"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"", + "innertube_context_client_version\":\"([0-9\\.]+?)\"", + "client.version=([0-9\\.]+)"}; + private static final String[] INNERTUBE_API_KEY_REGEXES = + {"INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", + "innertubeApiKey\":\"([0-9a-zA-Z_-]+?)\""}; + private static final String INNERTUBE_CLIENT_NAME_REGEX = + "INNERTUBE_CONTEXT_CLIENT_NAME\":([0-9]+?),"; private static final String CONTENT_PLAYBACK_NONCE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; @@ -484,12 +494,33 @@ public final class YoutubeParsingHelper { return hardcodedClientVersionAndKeyValid.get(); } - private static void extractClientVersionAndKey() throws IOException, ExtractionException { + + private static void extractClientVersionAndKeyFromSwJs() + throws IOException, ExtractionException { + if (keyAndVersionExtracted) { + return; + } + final String url = "https://www.youtube.com/sw.js"; + final Map> headers = new HashMap<>(); + headers.put("Origin", Collections.singletonList("https://www.youtube.com")); + headers.put("Referer", Collections.singletonList("https://www.youtube.com")); + final String response = getDownloader().get(url, headers).responseBody(); + try { + clientVersion = getStringResultFromRegexArray(response, + INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES, 1); + key = getStringResultFromRegexArray(response, INNERTUBE_API_KEY_REGEXES, 1); + } catch (final Parser.RegexException e) { + throw new ParsingException("Could not extract YouTube WEB InnerTube client version and API key from sw.js", e); + } + keyAndVersionExtracted = true; + } + + private static void extractClientVersionAndKeyFromHtmlSearchResultsPage() + throws IOException, ExtractionException { // Don't extract the client version and the InnerTube key if it has been already extracted if (keyAndVersionExtracted) { return; } - // 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 Map> headers = new HashMap<>(); @@ -526,21 +557,10 @@ public final class YoutubeParsingHelper { } } - String contextClientVersion; - final String[] patterns = { - "INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"", - "innertube_context_client_version\":\"([0-9\\.]+?)\"", - "client.version=([0-9\\.]+)" - }; - for (final String pattern : patterns) { - try { - contextClientVersion = Parser.matchGroup1(pattern, html); - if (!isNullOrEmpty(contextClientVersion)) { - clientVersion = contextClientVersion; - break; - } - } catch (final Parser.RegexException ignored) { - } + try { + clientVersion = getStringResultFromRegexArray(html, + INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES, 1); + } catch (final Parser.RegexException ignored) { } if (!isNullOrEmpty(clientVersion) && !isNullOrEmpty(shortClientVersion)) { @@ -548,13 +568,10 @@ public final class YoutubeParsingHelper { } try { - key = Parser.matchGroup1("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", html); - } catch (final Parser.RegexException e1) { - try { - key = Parser.matchGroup1("innertubeApiKey\":\"([0-9a-zA-Z_-]+?)\"", html); - } catch (final Parser.RegexException e2) { - throw new ParsingException("Could not extract client version and key"); - } + key = getStringResultFromRegexArray(html, INNERTUBE_API_KEY_REGEXES, 1); + } catch (final Parser.RegexException e) { + throw new ParsingException( + "Could not extract YouTube WEB InnerTube client version and API key from HTML search results page"); } keyAndVersionExtracted = true; } @@ -567,7 +584,11 @@ public final class YoutubeParsingHelper { return clientVersion; } - extractClientVersionAndKey(); + try { + extractClientVersionAndKeyFromSwJs(); + } catch (final Exception e) { + extractClientVersionAndKeyFromHtmlSearchResultsPage(); + } if (keyAndVersionExtracted) { return clientVersion; @@ -588,7 +609,11 @@ public final class YoutubeParsingHelper { return key; } - extractClientVersionAndKey(); + try { + extractClientVersionAndKeyFromSwJs(); + } catch (final Exception e) { + extractClientVersionAndKeyFromHtmlSearchResultsPage(); + } if (keyAndVersionExtracted) { return key; @@ -682,8 +707,8 @@ public final class YoutubeParsingHelper { return response.responseBody().length() > 500 && response.responseCode() == 200; } - public static String[] getYoutubeMusicKey() throws IOException, ReCaptchaException, - Parser.RegexException { + public static String[] getYoutubeMusicKey() + throws IOException, ReCaptchaException, Parser.RegexException { if (youtubeMusicKey != null && youtubeMusicKey.length == 3) { return youtubeMusicKey; } @@ -692,40 +717,34 @@ public final class YoutubeParsingHelper { return youtubeMusicKey; } - final String url = "https://music.youtube.com/"; - final Map> headers = new HashMap<>(); - addCookieHeader(headers); - final String html = getDownloader().get(url, headers).responseBody(); + String musicClientVersion = null; + String musicKey = null; + String musicClientName = null; - String innertubeApiKey; try { - innertubeApiKey = Parser.matchGroup1("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", html); - } catch (final Parser.RegexException e) { - innertubeApiKey = Parser.matchGroup1("innertube_api_key\":\"([0-9a-zA-Z_-]+?)\"", html); + final String url = "https://music.youtube.com/sw.js"; + final Map> headers = new HashMap<>(); + headers.put("Origin", Collections.singletonList("https://music.youtube.com")); + headers.put("Referer", Collections.singletonList("https://music.youtube.com")); + final String response = getDownloader().get(url, headers).responseBody(); + musicClientVersion = getStringResultFromRegexArray(response, + INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES, 1); + musicKey = getStringResultFromRegexArray(response, + INNERTUBE_API_KEY_REGEXES, 1); + musicClientName = Parser.matchGroup1(INNERTUBE_CLIENT_NAME_REGEX, response); + } catch (final Exception e) { + final String url = "https://music.youtube.com/"; + final Map> headers = new HashMap<>(); + addCookieHeader(headers); + final String html = getDownloader().get(url, headers).responseBody(); + + musicKey = getStringResultFromRegexArray(html, INNERTUBE_API_KEY_REGEXES, 1); + musicClientVersion = getStringResultFromRegexArray(html, + INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES); + musicClientName = Parser.matchGroup1(INNERTUBE_CLIENT_NAME_REGEX, html); } - final String innertubeClientName - = Parser.matchGroup1("INNERTUBE_CONTEXT_CLIENT_NAME\":([0-9]+?),", html); - - String innertubeClientVersion; - try { - innertubeClientVersion = Parser.matchGroup1( - "INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"", html); - } catch (final Parser.RegexException e) { - try { - innertubeClientVersion = Parser.matchGroup1( - "INNERTUBE_CLIENT_VERSION\":\"([0-9\\.]+?)\"", html); - } catch (final Parser.RegexException ee) { - innertubeClientVersion = Parser.matchGroup1( - "innertube_context_client_version\":\"([0-9\\.]+?)\"", html); - } - } - - youtubeMusicKey = new String[]{ - innertubeApiKey, - innertubeClientName, - innertubeClientVersion - }; + youtubeMusicKey = new String[] { musicKey, musicClientName, musicClientVersion }; return youtubeMusicKey; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java index 96789856b..3654061fc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java @@ -1,3 +1,23 @@ +/* + * Created by Christian Schabesberger on 02.02.16. + * + * Copyright (C) Christian Schabesberger 2016 + * Parser.java is part of NewPipe Extractor. + * + * NewPipe Extractor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe Extractor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe Extractor. If not, see . + */ + package org.schabi.newpipe.extractor.utils; import org.nibor.autolink.LinkExtractor; @@ -5,39 +25,21 @@ import org.nibor.autolink.LinkSpan; import org.nibor.autolink.LinkType; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import javax.annotation.Nonnull; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.schabi.newpipe.extractor.utils.Utils.UTF_8; -/* - * Created by Christian Schabesberger on 02.02.16. - * - * Copyright (C) Christian Schabesberger 2016 - * Parser.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - /** - * avoid using regex !!! + * Avoid using regex !!! */ public final class Parser { @@ -66,8 +68,9 @@ public final class Parser { return matchGroup(Pattern.compile(pattern), input, group); } - public static String matchGroup(final Pattern pat, final String input, final int group) - throws RegexException { + public static String matchGroup(@Nonnull final Pattern pat, + final String input, + final int group) throws RegexException { final Matcher matcher = pat.matcher(input); final boolean foundMatch = matcher.find(); if (foundMatch) { @@ -75,9 +78,9 @@ public final class Parser { } else { // only pass input to exception message when it is not too long if (input.length() > 1024) { - throw new RegexException("failed to find pattern \"" + pat.pattern() + "\""); + throw new RegexException("Failed to find pattern \"" + pat.pattern() + "\""); } else { - throw new RegexException("failed to find pattern \"" + pat.pattern() + throw new RegexException("Failed to find pattern \"" + pat.pattern() + "\" inside of \"" + input + "\""); } } @@ -89,14 +92,15 @@ public final class Parser { return mat.find(); } - public static boolean isMatch(final Pattern pattern, final String input) { + public static boolean isMatch(@Nonnull final Pattern pattern, final String input) { final Matcher mat = pattern.matcher(input); return mat.find(); } - public static Map compatParseMap(final String input) + @Nonnull + public static Map compatParseMap(@Nonnull final String input) throws UnsupportedEncodingException { - final Map map = new HashMap<>(); + final Map map = new HashMap<>(); for (final String arg : input.split("&")) { final String[] splitArg = arg.split("="); if (splitArg.length > 1) { @@ -108,9 +112,10 @@ public final class Parser { return map; } + @Nonnull public static String[] getLinksFromString(final String txt) throws ParsingException { try { - final ArrayList links = new ArrayList<>(); + final List links = new ArrayList<>(); final LinkExtractor linkExtractor = LinkExtractor.builder() .linkTypes(EnumSet.of(LinkType.URL, LinkType.WWW)) .build(); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java index 124a9bc8d..335244857 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java @@ -2,6 +2,8 @@ package org.schabi.newpipe.extractor.utils; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; @@ -10,6 +12,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -36,7 +39,8 @@ public final class Utils { * @param toRemove string to remove non-digit chars * @return a string that contains only digits */ - public static String removeNonDigitCharacters(final String toRemove) { + @Nonnull + public static String removeNonDigitCharacters(@Nonnull final String toRemove) { return toRemove.replaceAll("\\D+", ""); } @@ -52,8 +56,8 @@ public final class Utils { * @param numberWord string to be converted to a long * @return a long */ - public static long mixedNumberWordToLong(final String numberWord) throws NumberFormatException, - ParsingException { + public static long mixedNumberWordToLong(final String numberWord) + throws NumberFormatException, ParsingException { String multiplier = ""; try { multiplier = Parser.matchGroup("[\\d]+([\\.,][\\d]+)?([KMBkmb])+", numberWord, 2); @@ -94,7 +98,7 @@ public final class Utils { return null; } - if (!url.isEmpty() && url.startsWith(HTTP)) { + if (url.startsWith(HTTP)) { return HTTPS + url.substring(HTTP.length()); } return url; @@ -108,7 +112,9 @@ public final class Utils { * @param parameterName the pattern that will be used to check the url * @return a string that contains the value of the query parameter or null if nothing was found */ - public static String getQueryValue(final URL url, final String parameterName) { + @Nullable + public static String getQueryValue(@Nonnull final URL url, + final String parameterName) { final String urlQuery = url.getQuery(); if (urlQuery != null) { @@ -144,11 +150,12 @@ public final class Utils { * @param url the string to be converted to a URL-Object * @return a URL-Object containing the url */ + @Nonnull public static URL stringToURL(final String url) throws MalformedURLException { try { return new URL(url); } catch (final MalformedURLException e) { - // if no protocol is given try prepending "https://" + // If no protocol is given try prepending "https://" if (e.getMessage().equals("no protocol: " + url)) { return new URL(HTTPS + url); } @@ -157,8 +164,8 @@ public final class Utils { } } - public static boolean isHTTP(final URL url) { - // make sure its http or https + public static boolean isHTTP(@Nonnull final URL url) { + // Make sure it's HTTP or HTTPS final String protocol = url.getProtocol(); if (!protocol.equals("http") && !protocol.equals("https")) { return false; @@ -180,7 +187,7 @@ public final class Utils { return url; } - public static String removeUTF8BOM(final String s) { + public static String removeUTF8BOM(@Nonnull final String s) { String result = s; if (result.startsWith("\uFEFF")) { result = result.substring(1); @@ -198,7 +205,7 @@ public final class Utils { } catch (final MalformedURLException e) { final String message = e.getMessage(); if (message.startsWith("unknown protocol: ")) { - // return just the protocol (e.g. vnd.youtube) + // Return just the protocol (e.g. vnd.youtube) return message.substring("unknown protocol: ".length()); } @@ -214,17 +221,16 @@ public final class Utils { * @return an url with no Google search redirects */ public static String followGoogleRedirectIfNeeded(final String url) { - // if the url is a redirect from a Google search, extract the actual url + // If the url is a redirect from a Google search, extract the actual URL try { final URL decoded = Utils.stringToURL(url); if (decoded.getHost().contains("google") && decoded.getPath().equals("/url")) { - return URLDecoder.decode(Parser.matchGroup1("&url=([^&]+)(?:&|$)", url), - UTF_8); + return URLDecoder.decode(Parser.matchGroup1("&url=([^&]+)(?:&|$)", url), UTF_8); } } catch (final Exception ignored) { } - // url is not a google search redirect + // URL is not a Google search redirect return url; } @@ -232,13 +238,29 @@ public final class Utils { return str == null || str.isEmpty(); } - // can be used for JsonArrays + /** + * Checks if a collection is null or empty. + * + *

+ * This method can be also used for {@link com.grack.nanojson.JsonArray JsonArray}s. + *

+ * @param collection the collection on which check if it's null or empty + * @return whether the collection is null or empty + */ public static boolean isNullOrEmpty(final Collection collection) { return collection == null || collection.isEmpty(); } - // can be used for JsonObjects - public static boolean isNullOrEmpty(final Map map) { + /** + * Checks if a {@link Map map} is null or empty. + * + *

+ * This method can be also used for {@link com.grack.nanojson.JsonObject JsonObject}s. + *

+ * @param map the {@link Map map} on which check if it's null or empty + * @return whether the {@link Map map} is null or empty + */ + public static boolean isNullOrEmpty(final Map map) { return map == null || map.isEmpty(); } @@ -261,8 +283,9 @@ public final class Utils { return true; } + @Nonnull public static String join(final CharSequence delimiter, - final Iterable elements) { + @Nonnull final Iterable elements) { final StringBuilder stringBuilder = new StringBuilder(); final Iterator iterator = elements.iterator(); while (iterator.hasNext()) { @@ -274,11 +297,14 @@ public final class Utils { return stringBuilder.toString(); } - public static String join(final String delimiter, final String mapJoin, - final Map elements) { + @Nonnull + public static String join( + final String delimiter, + final String mapJoin, + @Nonnull final Map elements) { final List list = new LinkedList<>(); - for (final Map.Entry entry : elements - .entrySet()) { + for (final Map.Entry entry + : elements.entrySet()) { list.add(entry.getKey() + mapJoin + entry.getValue()); } return join(delimiter, list); @@ -287,10 +313,109 @@ public final class Utils { /** * Concatenate all non-null, non-empty and strings which are not equal to "null". */ + @Nonnull public static String nonEmptyAndNullJoin(final CharSequence delimiter, final String[] elements) { - final List list = new java.util.ArrayList<>(Arrays.asList(elements)); + final List list = new ArrayList<>(Arrays.asList(elements)); list.removeIf(s -> isNullOrEmpty(s) || s.equals("null")); return join(delimiter, list); } + + /** + * Find the result of an array of string regular expressions inside an input on the first + * group ({@code 0}). + * + * @param input the input on which using the regular expressions + * @param regexes the string array of regular expressions + * @return the result + * @throws Parser.RegexException if none of the patterns match the input + */ + @Nonnull + public static String getStringResultFromRegexArray(@Nonnull final String input, + @Nonnull final String[] regexes) + throws Parser.RegexException { + return getStringResultFromRegexArray(input, regexes, 0); + } + + /** + * Find the result of an array of {@link Pattern}s inside an input on the first group + * ({@code 0}). + * + * @param input the input on which using the regular expressions + * @param regexes the {@link Pattern} array + * @return the result + * @throws Parser.RegexException if none of the patterns match the input + */ + @Nonnull + public static String getStringResultFromRegexArray(@Nonnull final String input, + @Nonnull final Pattern[] regexes) + throws Parser.RegexException { + return getStringResultFromRegexArray(input, regexes, 0); + } + + /** + * Find the result of an array of string regular expressions inside an input on a specific + * group. + * + * @param input the input on which using the regular expressions + * @param regexes the string array of regular expressions + * @param group the group to match + * @return the result + * @throws Parser.RegexException if none of the patterns match the input, or at least in the + * specified group + */ + @Nonnull + public static String getStringResultFromRegexArray(@Nonnull final String input, + @Nonnull final String[] regexes, + final int group) + throws Parser.RegexException { + String result = null; + for (final String regex : regexes) { + try { + result = Parser.matchGroup(regex, input, group); + if (result != null) { + // Continue if the result is null + break; + } + } catch (final Parser.RegexException ignored) { + } + } + if (result == null) { + throw new Parser.RegexException("No regex matched the input on group " + group); + } + return result; + } + + /** + * Find the result of an array of {@link Pattern}s inside an input on a specific + * group. + * + * @param input the input on which using the regular expressions + * @param regexes the {@link Pattern} array + * @param group the group to match + * @return the result + * @throws Parser.RegexException if none of the patterns match the input, or at least in the + * specified group + */ + @Nonnull + public static String getStringResultFromRegexArray(@Nonnull final String input, + @Nonnull final Pattern[] regexes, + final int group) + throws Parser.RegexException { + String result = null; + for (final Pattern regex : regexes) { + try { + result = Parser.matchGroup(regex, input, group); + if (result != null) { + // Continue if the result is null + break; + } + } catch (final Parser.RegexException ignored) { + } + } + if (result == null) { + throw new Parser.RegexException("No regex matched the input on group " + group); + } + return result; + } }