8000 Add reddit support · DuncteBot/skybot-source-managers@a8cad49 · GitHub
[go: up one dir, main page]

Skip to content
This repository was archived by the owner on Nov 26, 2023. It is now read-only.

Commit a8cad49

Browse files
committed
Add reddit support
1 parent 92a3fb1 commit a8cad49

File tree

5 files changed

+286
-0
lines changed

5 files changed

+286
-0
lines changed

build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,13 @@ dependencies {
4444
implementation(group = "com.github.duncte123", name = "lavaplayer", version = "dd595a1")
4545
// api(group = "com.sedmelluq", name = "lavaplayer", version = "1.3.33")
4646
implementation(group = "io.sentry", name = "sentry-logback", version = "1.7.17")
47+
48+
implementation(group = "com.google.code.findbugs", name = "jsr305", version = "3.0.2")
4749
}
4850

4951
configure<JavaPluginConvention> {
5052
sourceCompatibility = JavaVersion.VERSION_11
53+
targetCompatibility = JavaVersion.VERSION_11
5154
}
5255

5356
tasks.withType<Wrapper> {

src/main/java/com/dunctebot/sourcemanagers/DuncteBotSources.java

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.dunctebot.sourcemanagers.extra.YoutubeContextFilterOverride;
2121
import com.dunctebot.sourcemanagers.getyarn.GetyarnAudioSourceManager;
2222
import com.dunctebot.sourcemanagers.pornhub.PornHubAudioSourceManager;
23+
import com.dunctebot.sourcemanagers.reddit.RedditAudioSourceManager;
2324
import com.dunctebot.sourcemanagers.speech.SpeechAudioSourceManager;
2425
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
2526
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
@@ -39,6 +40,7 @@ public static void registerCustom(AudioPlayerManager playerManager, String speec
3940
playerManager.registerSourceManager(new ClypitAudioSourceManager());
4041
playerManager.registerSourceManager(new SpeechAudioSourceManager(speechLanguage));
4142
playerManager.registerSourceManager(new PornHubAudioSourceManager());
43+
playerManager.registerSourceManager(new RedditAudioSourceManager());
4244

4345
}
4446
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2020 Duncan "duncte123" Sterken
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.dunctebot.sourcemanagers;
18+
19+
public class Utils {
20+
21+
public static boolean isURL(String url) {
22+
return url.matches("^https?:\\/\\/[-a-zA-Z0-9+&@#\\/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#\\/%=~_|]");
23+
}
24+
25+
}
< 10000 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2020 Duncan "duncte123" Sterken
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.dunctebot.sourcemanagers.reddit;
18+
19+
import com.dunctebot.sourcemanagers.AbstractDuncteBotHttpSource;
20+
import com.dunctebot.sourcemanagers.AudioTrackInfoWithImage;
21+
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
22+
import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools;
23+
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
24+
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
25+
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
26+
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
27+
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
28+
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
29+
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
30+
import org.apache.commons.io.IOUtils;
31+
import org.apache.http.client.methods.CloseableHttpResponse;
32+
import org.apache.http.client.methods.HttpGet;
33+
34+
import javax.annotation.Nullable;
35+
import java.io.DataInput;
36+
import java.io.DataOutput;
37+
import java.io.IOException;
38+
import java.nio.charset.StandardCharsets;
39+
import java.util.regex.Matcher;
40+
import java.util.regex.Pattern;
41+
42+
import static com.dunctebot.sourcemanagers.Utils.isURL;
43+
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON;
44+
import static com.sedmelluq.discord.lavaplayer.tools.JsonBrowser.NULL_BROWSER;
45+
import static com.dunctebot.sourcemanagers.reddit.RedditAudioTrack.getPlaybackUrl;
46+
47+
public class RedditAudioSourceManager extends AbstractDuncteBotHttpSource {
48+
private static final Pattern FULL_LINK_REGEX = Pattern.compile("https:\\/\\/(?:www|old)\\.reddit\\.com\\/r\\/(?:[^\\/]+)\\/(?:[^\\/]+)\\/([^\\/]+)(?:\\/?(?:[^\\/]+)?\\/?)?");
49+
private static final Pattern VIDEO_LINK_REGEX = Pattern.compile("https:\\/\\/v\\.redd\\.it\\/([^\\/]+)(?:.*)?");
50+
51+
public RedditAudioSourceManager() {
52+
this.configureBuilder(
53+
(builder) -> builder.setUserAgent("Mozilla/5.0 (compatible; https://github.com/DuncteBot/skybot-source-managers)")
54+
);
55+
}
56+
57+
@Override
58+
public String getSourceName() {
59+
return "reddit";
60+
}
61+
62+
@Override
63+
public AudioItem loadItem(DefaultAudioPlayerManager manager, AudioReference reference) {
64+
final String identifier = reference.identifier;
65+
final Matcher fullLink = FULL_LINK_REGEX.matcher(identifier);
66+
67+
// If it is a full link to a reddit post we can extract the id easily
68+
// and send that to fetch the json and build the track
69+
if (fullLink.matches()) {
70+
final String group = fullLink.group(fullLink.groupCount());
71+
final JsonBrowser data = this.fetchJson(group);
72+
73+
return this.buildTrack(data, identifier);
74+
}
75+
76+
final Matcher videoLink = VIDEO_LINK_REGEX.matcher(identifier);
77+
78+
// If we have a short video link we firstly need to follow all redirects
79+
if (videoLink.matches()) {
80+
// Once we have the link we can extract the post id and build the track the normal way
81+
final String actualRedditUrl = this.fetchRedirectUrl(identifier);
82+
final String id = this.getPostId(actualRedditUrl);
83+
final JsonBrowser data = this.fetchJson(id);
84+
85+
return this.buildTrack(data, actualRedditUrl);
86+
}
87+
88+
return null;
89+
}
90+
91+
@Override
92+
public boolean isTrackEncodable(AudioTrack track) {
93+
return true;
94+
}
95+
96+
@Override
97+
public void encodeTrack(AudioTrack track, DataOutput output) {
98+
// nothing to encode
99+
}
100+
101+
@Override
102+
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) {
103+
return new RedditAudioTrack(trackInfo, this);
104+
}
105+
106+
private String getPostId(String url) {
107+
final Matcher matcher = FULL_LINK_REGEX.matcher(url);
108+
109+
if (matcher.matches()) {
110+
return matcher.group(matcher.groupCount());
111+
}
112+
113+
return url;
114+
}
115+
116+
private String fetchRedirectUrl(String vRedditUrl) {
117+
final HttpGet httpGet = new HttpGet(vRedditUrl);
118+
final HttpInterface httpInterface = this.getHttpInterface();
119+
120+
// Follow all redirects until there are no more to follow and return that
121+
try (final CloseableHttpResponse ignored = httpInterface.execute(httpGet)) {
122+
return httpInterface.getFinalLocation().toString();
123+
}
124+
catch (IOException e) {
125+
throw ExceptionTools.wrapUnfriendlyExceptions("Could not load data from reddit", COMMON, e);
126+
}
127+
}
128+
129+
@Nullable
130+
private JsonBrowser fetchJson(String pageURl) {
131+
// Fetch the json from the reddit api so we don't get any useless stuff we don't care about
132+
final HttpGet httpGet = new HttpGet("https://api.reddit.com/api/info/?id=t3_" + pageURl);
133+
134+
try (final CloseableHttpResponse response = this.getHttpInterface().execute(httpGet)) {
135+
final String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
136+
final JsonBrowser child = JsonBrowser.parse(content).get("data").get("children").index(0);
137+
138+
// If we have nothing in the children array we can safely return null
139+
if (child.equals(NULL_BROWSER)) {
140+
return null;
141+
}
142+
143+
return child.get("data");
144+
}
145+
catch (IOException e) {
146+
throw ExceptionTools.wrapUnfriendlyExceptions("Could not load data from reddit", COMMON, e);
147+
}
148+
}
149+
150+
private boolean canPlayAudio(String id) {
151+
final HttpGet httpGet = new HttpGet(getPlaybackUrl(id));
152+
153+
// Probe the audio and check the response code, if it is 200 we have some audio
154+
try (final CloseableHttpResponse response = this.getHttpInterface().execute(httpGet)) {
155+
return response.getStatusLine().getStatusCode() == 200;
156+
}
157+
catch (IOException e) {
158+
return false;
159+
}
160+
}
161+
162+
private RedditAudioTrack buildTrack(@Nullable JsonBrowser data, String pageURl) {
163+
// If we don't have any data we can return null
164+
if (data == null) {
165+
return null;
166+
}
167+
168+
final String postHint = data.get("post_hint").safeText();
169+
170+
// Check if this is a video post that is hosted on reddit
171+
// we cannot play other types
172+
if (!"hosted:video".equals(postHint)) {
173+
throw new FriendlyException("This video is not hosted on the reddit website," +
174+
" only videos hosted on the reddit website can be played", COMMON, null);
175+
}
176+
177+
final JsonBrowser media = data.get("media").get("reddit_video");
178+
final String url = data.get("url").safeText();
179+
180+
final Matcher videoLink = VIDEO_LINK_REGEX.matcher(url);
181+
182+
// This should never happen unless my regex is wrong
183+
if (!videoLink.matches()) {
184+
return null;
185+
}
186+
187+
final String videoId = videoLink.group(videoLink.groupCount());
188+
189+
// Probe the audio to check if we can actually play it (there's probably a better way with the dash playlists)
190+
if (!this.canPlayAudio(videoId)) {
191+
throw new FriendlyException("This video does not have audio", COMMON, null);
192+
}
193+
194+
String thumbnail = data.get("thumbnail").safeText();
195+
196+
// Fallback to null if the thumbnail is not a url
197+
if (!isURL(thumbnail)) {
198+
thumbnail = null;
199+
}
200+
201+
return new RedditAudioTrack(
202+
new AudioTrackInfoWithImage(
203+
data.get("title").safeText(),
204+
"u/" + data.get("author").safeText(),
205+
Long.parseLong(media.get("duration").safeText()) * 1000,
206+
videoId,
207+
false,
208+
pageURl,
209+
thumbnail
210+
),
211+
this
212+
);
213+
}
214+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2020 Duncan "duncte123" Sterken
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.dunctebot.sourcemanagers.reddit;
18+
19+
import com.dunctebot.sourcemanagers.AbstractDuncteBotHttpSource;
20+
import com.dunctebot.sourcemanagers.MpegTrack;
21+
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
22+
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
23+
24+
public class RedditAudioTrack extends MpegTrack {
25+
public RedditAudioTrack(AudioTrackInfo trackInfo, AbstractDuncteBotHttpSource manager) {
26+
super(trackInfo, manager);
27+
}
28+
29+
@Override
30+
protected String getPlaybackUrl() {
31+
return getPlaybackUrl(this.trackInfo.identifier);
32+
}
33+
34+
static String getPlaybackUrl(String id) {
35+
return "https://v.redd.it/" + id + "/audio?source=fallback";
36+
}
37+
38+
@Override
39+
protected AudioTrack makeShallowClone() {
40+
return new RedditAudioTrack(this.trackInfo, this.getSourceManager());
41+
}
42+
}

0 commit comments

Comments
 (0)
0