Skip to content

Commit d22786b

Browse files
committed
Merge branch 'master' into dev
2 parents 5b93db0 + 4c633c2 commit d22786b

10 files changed

Lines changed: 198 additions & 43 deletions

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeChannelLinkHandlerFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public String getId(String url) throws ParsingException {
4646
URL urlObj = Utils.stringToURL(url);
4747
String path = urlObj.getPath();
4848

49-
if (!(YoutubeParsingHelper.isYoutubeURL(urlObj) || urlObj.getHost().equalsIgnoreCase("hooktube.com"))) {
49+
if (!Utils.isHTTP(urlObj) || !(YoutubeParsingHelper.isYoutubeURL(urlObj) ||
50+
YoutubeParsingHelper.isInvidioURL(urlObj) || YoutubeParsingHelper.isHooktubeURL(urlObj))) {
5051
throw new ParsingException("the URL given is not a Youtube-URL");
5152
}
5253

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeParsingHelper.java

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,25 @@ public class YoutubeParsingHelper {
3030
private YoutubeParsingHelper() {
3131
}
3232

33-
private static boolean isHTTP(URL url) {
34-
// make sure its http or https
35-
String protocol = url.getProtocol();
36-
if (!protocol.equals("http") && !protocol.equals("https")) {
37-
return false;
38-
}
39-
40-
boolean usesDefaultPort = url.getPort() == url.getDefaultPort();
41-
boolean setsNoPort = url.getPort() == -1;
42-
43-
return setsNoPort || usesDefaultPort;
44-
}
45-
4633
public static boolean isYoutubeURL(URL url) {
47-
// make sure its http or https
48-
if (!isHTTP(url))
49-
return false;
50-
51-
// make sure its a known youtube url
5234
String host = url.getHost();
5335
return host.equalsIgnoreCase("youtube.com") || host.equalsIgnoreCase("www.youtube.com")
5436
|| host.equalsIgnoreCase("m.youtube.com");
5537
}
5638

57-
public static boolean isYoutubeALikeURL(URL url) {
58-
// make sure its http or https
59-
if (!isHTTP(url))
60-
return false;
39+
public static boolean isYoutubeServiceURL(URL url) {
40+
String host = url.getHost();
41+
return host.equalsIgnoreCase("www.youtube-nocookie.com") || host.equalsIgnoreCase("youtu.be");
42+
}
6143

62-
// make sure its a known youtube url
44+
public static boolean isHooktubeURL(URL url) {
6345
String host = url.getHost();
64-
return host.equalsIgnoreCase("youtube.com") || host.equalsIgnoreCase("www.youtube.com")
65-
|| host.equalsIgnoreCase("m.youtube.com") || host.equalsIgnoreCase("www.youtube-nocookie.com")
66-
|| host.equalsIgnoreCase("youtu.be") || host.equalsIgnoreCase("hooktube.com");
46+
return host.equalsIgnoreCase("hooktube.com");
47+
}
48+
49+
public static boolean isInvidioURL(URL url) {
50+
String host = url.getHost();
51+
return host.equalsIgnoreCase("invidio.us") || host.equalsIgnoreCase("www.invidio.us");
6752
}
6853

6954
public static long parseDurationString(String input)

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@ public String getId(String url) throws ParsingException {
2525
try {
2626
URL urlObj = Utils.stringToURL(url);
2727

28-
if (!YoutubeParsingHelper.isYoutubeURL(urlObj)) {
28+
if (!Utils.isHTTP(urlObj) || !(YoutubeParsingHelper.isYoutubeURL(urlObj)
29+
|| YoutubeParsingHelper.isInvidioURL(urlObj))) {
2930
throw new ParsingException("the url given is not a Youtube-URL");
3031
}
3132

33+
String path = urlObj.getPath();
34+
if (!path.equals("/watch" ) && !path.equals("/playlist")) {
35+
throw new ParsingException("the url given is neither a video nor a playlist URL");
36+
}
37+
3238
String listID = Utils.getQueryValue(urlObj, "list");
3339

3440
if (listID == null) {

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeStreamLinkHandlerFactory.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public String getId(String urlString) throws ParsingException, IllegalArgumentEx
6060
URI uri = new URI(urlString);
6161
String scheme = uri.getScheme();
6262

63-
if (scheme != null && scheme.equals("vnd.youtube")) {
63+
if (scheme != null && (scheme.equals("vnd.youtube") || scheme.equals("vnd.youtube.launch"))) {
6464
String schemeSpecificPart = uri.getSchemeSpecificPart();
6565
if (schemeSpecificPart.startsWith("//")) {
6666
urlString = "https:" + schemeSpecificPart;
@@ -85,7 +85,9 @@ public String getId(String urlString) throws ParsingException, IllegalArgumentEx
8585
path = path.substring(1);
8686
}
8787

88-
if (!YoutubeParsingHelper.isYoutubeALikeURL(url)) {
88+
if (!Utils.isHTTP(url) || !(YoutubeParsingHelper.isYoutubeURL(url) ||
89+
YoutubeParsingHelper.isYoutubeServiceURL(url) || YoutubeParsingHelper.isHooktubeURL(url) ||
90+
YoutubeParsingHelper.isInvidioURL(url))) {
8991
if (host.equalsIgnoreCase("googleads.g.doubleclick.net")) {
9092
throw new FoundAdException("Error found ad: " + urlString);
9193
}
@@ -147,6 +149,21 @@ public String getId(String urlString) throws ParsingException, IllegalArgumentEx
147149
}
148150

149151
case "HOOKTUBE.COM": {
152+
if (path.startsWith("v/")) {
153+
String id = path.substring("v/".length());
154+
155+
return assertIsID(id);
156+
}
157+
if (path.startsWith("watch/")) {
158+
String id = path.substring("watch/".length());
159+
160+
return assertIsID(id);
161+
}
162+
// there is no break-statement here on purpose so the next code-block gets also run for hooktube
163+
}
164+
165+
case "WWW.INVIDIO.US":
166+
case "INVIDIO.US": { // code-block for hooktube.com and invidio.us
150167
if (path.equals("watch")) {
151168
String viewQueryValue = Utils.getQueryValue(url, "v");
152169
if (viewQueryValue != null) {
@@ -158,19 +175,9 @@ public String getId(String urlString) throws ParsingException, IllegalArgumentEx
158175

159176
return assertIsID(id);
160177
}
161-
if (path.startsWith("v/")) {
162-
String id = path.substring("v/".length());
163178

164-
return assertIsID(id);
165-
}
166-
if (path.startsWith("watch/")) {
167-
String id = path.substring("watch/".length());
168-
169-
return assertIsID(id);
170-
}
179+
break;
171180
}
172-
173-
break;
174181
}
175182

176183
throw new ParsingException("Error no suitable url: " + urlString);

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeTrendingLinkHandlerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ public boolean onAcceptUrl(final String url) {
4848
}
4949

5050
String urlPath = urlObj.getPath();
51-
return YoutubeParsingHelper.isYoutubeURL(urlObj) && urlPath.equals("/feed/trending");
51+
return Utils.isHTTP(urlObj) && (YoutubeParsingHelper.isYoutubeURL(urlObj) || YoutubeParsingHelper.isInvidioURL(urlObj)) && urlPath.equals("/feed/trending");
5252
}
5353
}

extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ public static URL stringToURL(String url) throws MalformedURLException {
120120
throw e;
121121
}
122122
}
123+
124+
public static boolean isHTTP(URL url) {
125+
// make sure its http or https
126+
String protocol = url.getProtocol();
127+
if (!protocol.equals("http") && !protocol.equals("https")) {
128+
return false;
129+
}
130+
131+
boolean usesDefaultPort = url.getPort() == url.getDefaultPort();
132+
boolean setsNoPort = url.getPort() == -1;
133+
134+
return setsNoPort || usesDefaultPort;
135+
}
123136

124137
public static String removeUTF8BOM(String s) {
125138
if (s.startsWith("\uFEFF")) {

extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelLinkHandlerFactoryTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public void acceptrUrlTest() throws ParsingException {
3737

3838
assertTrue(linkHandler.acceptUrl("https://hooktube.com/channel/UClq42foiSgl7sSpLupnugGA"));
3939
assertTrue(linkHandler.acceptUrl("https://hooktube.com/channel/UClq42foiSgl7sSpLupnugGA/videos?disable_polymer=1"));
40+
41+
assertTrue(linkHandler.acceptUrl("https://invidio.us/user/Gronkh"));
42+
assertTrue(linkHandler.acceptUrl("https://invidio.us/user/Netzkino/videos"));
43+
44+
assertTrue(linkHandler.acceptUrl("https://invidio.us/channel/UClq42foiSgl7sSpLupnugGA"));
45+
assertTrue(linkHandler.acceptUrl("https://invidio.us/channel/UClq42foiSgl7sSpLupnugGA/videos?disable_polymer=1"));
4046
}
4147

4248
@Test
@@ -53,5 +59,11 @@ public void getIdFromUrl() throws ParsingException {
5359

5460
assertEquals("channel/UClq42foiSgl7sSpLupnugGA", linkHandler.fromUrl("https://hooktube.com/channel/UClq42foiSgl7sSpLupnugGA").getId());
5561
assertEquals("channel/UClq42foiSgl7sSpLupnugGA", linkHandler.fromUrl("https://hooktube.com/channel/UClq42foiSgl7sSpLupnugGA/videos?disable_polymer=1").getId());
62+
63+
assertEquals("user/Gronkh", linkHandler.fromUrl("https://invidio.us/user/Gronkh").getId());
64+
assertEquals("user/Netzkino", linkHandler.fromUrl("https://invidio.us/user/Netzkino/videos").getId());
65+
66+
assertEquals("channel/UClq42foiSgl7sSpLupnugGA", linkHandler.fromUrl("https://invidio.us/channel/UClq42foiSgl7sSpLupnugGA").getId());
67+
assertEquals("channel/UClq42foiSgl7sSpLupnugGA", linkHandler.fromUrl("https://invidio.us/channel/UClq42foiSgl7sSpLupnugGA/videos?disable_polymer=1").getId());
5668
}
5769
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.schabi.newpipe.extractor.services.youtube;
2+
3+
import org.junit.BeforeClass;
4+
import org.junit.Test;
5+
import org.schabi.newpipe.Downloader;
6+
import org.schabi.newpipe.extractor.NewPipe;
7+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
8+
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubePlaylistLinkHandlerFactory;
9+
import org.schabi.newpipe.extractor.utils.Localization;
10+
11+
import static org.junit.Assert.*;
12+
13+
/**
14+
* Test for {@link YoutubePlaylistLinkHandlerFactory}
15+
*/
16+
public class YoutubePlaylistLinkHandlerFactoryTest {
17+
private static YoutubePlaylistLinkHandlerFactory linkHandler;
18+
19+
@BeforeClass
20+
public static void setUp() {
21+
linkHandler = YoutubePlaylistLinkHandlerFactory.getInstance();
22+
NewPipe.init(Downloader.getInstance(), new Localization("GB", "en"));
23+
}
24+
25+
@Test(expected = IllegalArgumentException.class)
26+
public void getIdWithNullAsUrl() throws ParsingException {
27+
linkHandler.fromId(null);
28+
}
29+
30+
@Test
31+
public void getIdfromYt() throws Exception {
32+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
33+
assertEquals("PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV", linkHandler.fromUrl("https://www.youtube.com/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV").getId());
34+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC&t=100").getId());
35+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://WWW.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC&t=100").getId());
36+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("HTTPS://www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC&t=100").getId());
37+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://www.youtube.com/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
38+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("http://www.youtube.com/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
39+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://m.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
40+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
41+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
42+
assertEquals("PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV", linkHandler.fromUrl("www.youtube.com/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV").getId());
43+
}
44+
45+
@Test
46+
public void testAcceptYtUrl() throws ParsingException {
47+
assertTrue(linkHandler.acceptUrl("https://www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
48+
assertTrue(linkHandler.acceptUrl("https://www.youtube.com/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
49+
assertTrue(linkHandler.acceptUrl("https://WWW.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dCI"));
50+
assertTrue(linkHandler.acceptUrl("HTTPS://www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
51+
assertTrue(linkHandler.acceptUrl("https://www.youtube.com/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
52+
assertTrue(linkHandler.acceptUrl("http://www.youtube.com/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
53+
assertTrue(linkHandler.acceptUrl("https://m.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
54+
assertTrue(linkHandler.acceptUrl("https://youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
55+
assertTrue(linkHandler.acceptUrl("www.youtube.com/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
56+
assertTrue(linkHandler.acceptUrl("www.youtube.com/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
57+
}
58+
59+
@Test
60+
public void testDeniesInvalidYtUrl() throws ParsingException {
61+
assertFalse(linkHandler.acceptUrl("https://www.youtube.com/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
62+
assertFalse(linkHandler.acceptUrl("https://www.youtube.com/feed/subscriptions?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
63+
assertFalse(linkHandler.acceptUrl("ftp://www.youtube.com/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
64+
assertFalse(linkHandler.acceptUrl("www.youtube.com:22/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
65+
assertFalse(linkHandler.acceptUrl("youtube . com/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
66+
assertFalse(linkHandler.acceptUrl("?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
67+
}
68+
69+
@Test
70+
public void testAcceptInvidioUrl() throws ParsingException {
71+
assertTrue(linkHandler.acceptUrl("https://www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
72+
assertTrue(linkHandler.acceptUrl("https://www.invidio.us/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
73+
assertTrue(linkHandler.acceptUrl("https://WWW.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dCI"));
74+
assertTrue(linkHandler.acceptUrl("HTTPS://www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
75+
assertTrue(linkHandler.acceptUrl("https://www.invidio.us/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
76+
assertTrue(linkHandler.acceptUrl("http://www.invidio.us/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
77+
assertTrue(linkHandler.acceptUrl("https://invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
78+
assertTrue(linkHandler.acceptUrl("www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC"));
79+
assertTrue(linkHandler.acceptUrl("www.invidio.us/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
80+
}
81+
82+
@Test
83+
public void testDeniesInvalidInvidioUrl() throws ParsingException {
84+
assertFalse(linkHandler.acceptUrl("https://invidio.us/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
85+
assertFalse(linkHandler.acceptUrl("https://invidio.us/feed/subscriptions?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
86+
assertFalse(linkHandler.acceptUrl("ftp:/invidio.us/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
87+
assertFalse(linkHandler.acceptUrl("invidio.us:22/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
88+
assertFalse(linkHandler.acceptUrl("invidio . us/feed/trending?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
89+
assertFalse(linkHandler.acceptUrl("?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
90+
}
91+
92+
@Test
93+
public void testGetInvidioIdfromUrl() throws ParsingException {
94+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
95+
assertEquals("PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV", linkHandler.fromUrl("https://www.invidio.us/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV").getId());
96+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC&t=100").getId());
97+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://WWW.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC&t=100").getId());
98+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("HTTPS://www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC&t=100").getId());
99+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://www.invidio.us/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
100+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("http://www.invidio.us/watch?v=0JFM3PRZH-k&index=8&list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
101+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("https://invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
102+
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
103+
assertEquals("PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV", linkHandler.fromUrl("www.invidio.us/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV").getId());
104+
}
105+
}

0 commit comments

Comments
 (0)