1 | !7 |
2 | |
3 | static MapSO _renameClasses = litmap( |
4 | "DynDiscordHopper" := "DynShardedDiscordHopper", |
5 | "DynServerAwareDiscordBot" := "DynShardedServerAwareDiscordBot", |
6 | "DynTalkBot2" := "DynShardedTalkBot2"); |
7 | |
8 | set flag JDA40. |
9 | |
10 | // TODO: on restart, check if we actually are in vc with voiceChannelID |
11 | // (if permissions have changed) |
12 | |
13 | // Changes in this version: |
14 | // uploadImagesAsFiles |
15 | // !!!listeners fixed |
16 | // check if embed fields are longer than 1k |
17 | |
18 | // Note: dm owners only takes ONE LINE |
19 | // TODO: send dms only once when owner has two guilds |
20 | |
21 | !include once #1030357 // Discord Audio |
22 | |
23 | cmodule MKLabFM extends DynShardedTalkBot2<.ByServer> { |
24 | switchable S radioURL = "http://streamlive2.hearthis.at:8080/26450.ogg"; |
25 | switchable S currentSongURL = "https://widgets.autopo.st/widgets/public/MKLabFM/nowplaying.php"; |
26 | switchable S coverArtURL = "http://95.154.196.33:26687/playingart?sid=1&rand=<RNDINT>"; |
27 | switchable bool enableShadowStop = true; |
28 | switchable bool putSongInStatus = true; |
29 | transient double bufferSeconds = 30.0; |
30 | transient int grabInterval = 15; // try to grab frames quicker to see if it fixes the jumps |
31 | transient double considerDownInterval = 60.0; |
32 | |
33 | // restart stream when no frames received for this long |
34 | transient double autoRestartInterval = 20.0; |
35 | |
36 | switchable S madeBy = trim([[ |
37 | MKLab: <https://tinyurl.com/musikcolabradiofm> |
38 | Support channel: <https://discord.gg/u6E4muY> |
39 | Bot by: <https://BotCompany.de> |
40 | You like this bot? Donate: <https://paypal.me/BotCompany> |
41 | ]]); |
42 | |
43 | switchable S voteLink = "\n\nPlease help us grow by voting here: <https://discordbots.org/bot/625377645982384128>"; |
44 | |
45 | switchable S npMsg = "Now playing: <track> - Listen at }{ https://tinyurl.com/mklab-live-fm }{ #edm #music #radio #ibiza #dance #chillout #psy #live #miami #memes #detroit #techno #dj #synthwave #housemusic #deephouse #onair"; |
46 | |
47 | switchable bool uploadImagesAsFiles = true; |
48 | |
49 | transient SimpleCircularBuffer<byte[]> buffer; |
50 | transient AudioPlayer player; |
51 | switchable transient bool simulateStreamDown, restartNow; |
52 | transient long lastFrameReceived, lastActualFrameReceived; // sys time |
53 | transient long moduleStarted; // sys time |
54 | transient NotTooOften postRestarting = onlyEveryHour(); |
55 | transient S currentSong; |
56 | |
57 | // stats |
58 | transient new AtomicLong framesReceived; |
59 | transient new AtomicLong canProvideCalls; |
60 | transient new AtomicLong provideCalls; |
61 | transient new AtomicLong dataSent; |
62 | |
63 | O _getReloadData() { ret currentSong; } |
64 | void _setReloadData(S data) { currentSong = data; } |
65 | |
66 | void init { |
67 | super.init(); |
68 | myName = "!!!"; |
69 | makeByServer = () -> new ByServer; |
70 | dropPunctuation = false; |
71 | preprocessAtSelfToMyName = true; |
72 | ngbCommands = false; |
73 | sendToPostedLinesCRUD = false; |
74 | |
75 | // First thing, start the stream, so we can restart the radios |
76 | initStream(); |
77 | |
78 | onDiscordStarted(r { |
79 | temp enter(); |
80 | print("Bot back up"); |
81 | for (Long id, ByServer bs : cloneMap(dataByServer)) { pcall { |
82 | bs.restart(discord.getGuildById(id)); |
83 | }} |
84 | |
85 | postInChannel(nextGenBotsTestRoomID(), "Rebooted " + computerID() + "/" + dm_moduleID()); |
86 | }); |
87 | |
88 | onDiscordEvent(e -> { |
89 | if (e cast GenericGuildVoiceEvent) |
90 | getByServer(e.getGuild()).handleVoiceUpdateEvent(e); |
91 | }); |
92 | |
93 | doEveryAndNow(20.0, r grabSong); |
94 | doEvery(60.0, r updateVoiceChannelCount_allServers); |
95 | } |
96 | |
97 | void initStream { |
98 | buffer = new SimpleCircularBuffer(iround(bufferSeconds*50)); |
99 | player = lavaPlayer_createPlayer(); |
100 | lavaPlayer_printPlayerEvents(player); |
101 | print("Have player"); |
102 | |
103 | doEvery(grabInterval, r { |
104 | temp enter(); |
105 | if (!simulateStreamDown) { |
106 | AudioFrame frame = player.provide(); |
107 | if (frame != null) { |
108 | buffer.add(frame.getData()); |
109 | inc(framesReceived); |
110 | lastFrameReceived = lastActualFrameReceived = sysNow(); |
111 | } |
112 | } |
113 | }); |
114 | |
115 | doEvery(1.0, r { |
116 | temp enter(); |
117 | if (discord == null) ret; |
118 | if (restartNow || elapsedSeconds(lastFrameReceived) >= autoRestartInterval) { |
119 | if (postRestarting.yo()) |
120 | postInChannel(nextGenBotsTestRoomID(), "Restarting radio stream"); |
121 | simulateStreamDown = restartNow = false; |
122 | lastFrameReceived = sysNow(); |
123 | loadTrack(); |
124 | lastFrameReceived = sysNow(); |
125 | } |
126 | }); |
127 | } |
128 | |
129 | class ByServer extends DynShardedTalkBot2.ByServer { |
130 | bool shouldBePlaying; |
131 | long voiceChannelID, lastVoiceChannelID; |
132 | long songChannelID; |
133 | |
134 | transient S voiceChannelName; |
135 | transient VoiceChannel vc; |
136 | transient MyAudioSendHandler sendHandler; |
137 | transient Set<Long> usersInVoiceChannel = synchroSet(); |
138 | |
139 | synchronized S processSimplifiedLine(S s, O... _) enter { |
140 | try { |
141 | ret processSimplifiedLine2(s, _); |
142 | } catch print e { |
143 | if (cic(str(e), "InsufficientPermission")) |
144 | ret "There were insufficient permissions to perform this action."; |
145 | ret "Sorry, an error occurred trying to perform this action."; |
146 | } |
147 | } |
148 | |
149 | synchronized S processSimplifiedLine2(S s, O... _) { |
150 | try answer super.processSimplifiedLine(s, _); // add authorized users etc. |
151 | new Matches m; |
152 | |
153 | print("myPrefix", quote(myPrefix())); |
154 | s = dropMyPrefixOrNull(s); |
155 | print("dropped", quote(s)); |
156 | if (s == null) null; |
157 | optPar Message msg; |
158 | Guild guild = msg.getGuild(); |
159 | Member member = msg == null ? null : msg.getMember(); |
160 | |
161 | if (eqic(s, "help")) { |
162 | optPar MessageChannel channel; |
163 | |
164 | S syntax = trim([[ |
165 | `@me play | start` - start playing |
166 | `@me stop | pause` - stop playing |
167 | `@me leave` - leave voice channel |
168 | `@me help` - show this help |
169 | `@me np` - show currently playing song |
170 | `@me join` - join any voice channel |
171 | `@me set channel` - post now playing track names in current channel |
172 | `@me stop posting` - stop posting track names |
173 | [MORE] |
174 | ]].replace("\n[MORE]", renderChannelListIfNotOne(guild)) |
175 | .replace("@me ", myPrefix())) |
176 | + voteLink; |
177 | |
178 | new EmbedBuilder eb; |
179 | eb.setColor(Color.yellow); |
180 | eb.setAuthor("MKLab FM Bot", null, null); |
181 | eb.setTitle("Purpose", null); |
182 | eb.setDescription("I play MKLab FM, the best Electronic Dance Music Radio, in a voice channel of your choice."); |
183 | discordEmbed_addField(eb, "Syntax", syntax); |
184 | |
185 | if (authed(_)) |
186 | eb.addField("Admin commands", rtrim([[ |
187 | `@me guild count/guild names` - show stats |
188 | `@me reboot` - reboot the whole bot (if music stream is completely broken for some reason) |
189 | `@me dm owners: <message>` - DM all guild owners where radio is playing |
190 | `@me stream url http...` - set stream URL. current: $STREAMURL |
191 | `@me np url http...` - set current song URL. current: $NPURL |
192 | `@me cover url http...` - set cover art URL. current: $COVERURL |
193 | `@me cover url -` - disable cover art |
194 | `@me np msg Playing <track>...` - set nowplaying message. current: $NPMSG |
195 | `@me listeners` - full list of listeners |
196 | ]]) |
197 | .replace("@me ", myPrefix()) |
198 | .replace("$STREAMURL", backtickQuote(radioURL)) |
199 | .replace("$NPURL", backtickQuote(currentSongURL)) |
200 | .replace("$NPMSG", backtickQuote(npMsg)) |
201 | .replace("$COVERURL", backtickQuote(coverArtURL)), false); |
202 | |
203 | eb.addField("Made by", madeBy, false); |
204 | |
205 | eb.setThumbnail(imageSnippetURL(#1102722)); |
206 | channel.sendMessage(eb.build()).queue(); |
207 | null; |
208 | } |
209 | |
210 | if (guild == null) null; |
211 | |
212 | if (authed(_)) { |
213 | /*if "restart radios" |
214 | ret "OK" with restartRadios();*/ |
215 | if "reboot" { |
216 | doAfter(2.0, rEnter dm_reload); |
217 | ret "OK, rebooting bot"; |
218 | } |
219 | if "simulate stream down" { |
220 | simulateStreamDown = true; |
221 | ret "OK"; |
222 | } |
223 | if (swic_trim(s, "dm owners:", m)) |
224 | ret dmOwners($1); |
225 | |
226 | if (swic_trim(s, "stream url ", m) && isWebURL(m.rest())) { |
227 | if (!MKLabFM.this.setField(radioURL := m.rest())) ret "No change"; |
228 | restartNow = true; |
229 | ret "OK, restarting with radio URL " + radioURL; |
230 | } |
231 | |
232 | if (swic_trim(s, "np url ", m) && isWebURL(m.rest())) { |
233 | if (!MKLabFM.this.setField(currentSongURL := m.rest())) ret "No change"; |
234 | ret "Current song URL changed to " + currentSongURL; |
235 | } |
236 | |
237 | if (swic_trim(s, "cover url ", m)) { |
238 | if (!MKLabFM.this.setField(coverArtURL := m.rest())) ret "No change"; |
239 | ret "Cover art URL changed to " + coverArtURL; |
240 | } |
241 | |
242 | if (swic_trim(s, "np msg ", m)) { |
243 | if (!MKLabFM.this.setField(npMsg := m.rest())) ret "No change"; |
244 | ret "Nowplaying message changed to " + npMsg; |
245 | } |
246 | |
247 | if "listeners" |
248 | ret or2(listeningUsers_full(), "No listeners"); |
249 | } |
250 | |
251 | if (matchX2("play ...|start...|radio|radio on", s)) |
252 | ret action_play(guild, member); |
253 | |
254 | if (matchX2("stop ...|pause ...|quiet|silence|shut up|... off", s) && !match("stop posting", s)) |
255 | ret action_stop(guild); |
256 | |
257 | if (matchX2("leave|leave channel|leave voice channel", s)) |
258 | ret action_leave(guild); |
259 | |
260 | if (matchX2("join|join channel|join voice channel", s)) |
261 | ret action_join(guild, member); |
262 | |
263 | if (matchX2("... channel *", s, m) && isInteger($1)) |
264 | ret action_selectChannel(guild, parseInt($1)); |
265 | |
266 | if (matchX2("np|n p|... song ...|... playing ...", s)) |
267 | ret "MKLab FM is currently playing: " + (nempty(currentSong) ? backtickQuote(currentSong) : "Hmm. Not sure, I think something went wrong."); |
268 | |
269 | if (matchX2("set channel|post here|set np", s)) { |
270 | long channelID = longPar channelID(_); |
271 | if (channelID == 0) ret "Please do this in a channel"; |
272 | if (songChannelID != channelID) { |
273 | songChannelID = channelID; |
274 | change(); |
275 | } |
276 | doAfter(2.0, r postSong); |
277 | ret "OK, I will post the currently playing tracks in this channel. Type `" + atSelfOrMyName_space() + "stop posting` to stop."; |
278 | } |
279 | |
280 | if (matchX2("stop posting|no channel", s)) { |
281 | songChannelID = 0; |
282 | change(); |
283 | ret "OK"; |
284 | } |
285 | |
286 | if "users in channel" |
287 | ret "Users in voice channel: " + n2(usersInVoiceChannel); |
288 | |
289 | if "internal stats" |
290 | ret "Frames received: " + n2(framesReceived!) |
291 | + ", canProvide calls: " + n2(canProvideCalls!) |
292 | + ", provide calls: " + n2(provideCalls!) |
293 | + ", bytes sent: " + n2(dataSent!); |
294 | |
295 | null; |
296 | } |
297 | |
298 | S renderGuildCount() { |
299 | L<Guild> guilds = discord.getGuilds(); |
300 | int shouldPlayCount = 0, shadowCount = 0; |
301 | for (Guild guild : guilds) { |
302 | ByServer bs = getByServer(guild.getIdLong(), true); |
303 | if (bs != null) { |
304 | //if (bs.playing) ++playCount; |
305 | if (bs.shouldBePlaying) { |
306 | ++shouldPlayCount; |
307 | if (bs.shadowStopped()) ++shadowCount; |
308 | } |
309 | } |
310 | } |
311 | ret super.renderGuildCount() + ". " + nRadios(shouldPlayCount-shadowCount) + " playing to " + n2(listeningUsers(), "listener") + ", " + shadowCount + " playing without listeners"; |
312 | } |
313 | |
314 | void joinVoiceChannel(Guild guild) { |
315 | vc = guild.getVoiceChannelById(voiceChannelID); |
316 | guild.getAudioManager().openAudioConnection(vc); |
317 | voiceChannelName = vc.getName(); |
318 | updateVoiceChannelCount(); |
319 | } |
320 | |
321 | void updateVoiceChannelCount { |
322 | if (vc == null) ret with usersInVoiceChannel.clear(); |
323 | L<Member> members = vc.getMembers(); |
324 | int n = l(usersInVoiceChannel); |
325 | replaceCollection(usersInVoiceChannel, |
326 | mapNonNulls_pcall(members, m -> { |
327 | User user = m.getUser(); |
328 | ret user.isBot() ? null : user.getIdLong(); |
329 | })); |
330 | int n2 = l(usersInVoiceChannel); |
331 | if (n != n2) print(guildID + " voice channel count corrected: " + n + " -> " + n2); |
332 | } |
333 | |
334 | bool shadowStopped() { |
335 | ret enableShadowStop && empty(usersInVoiceChannel); |
336 | } |
337 | |
338 | class MyAudioSendHandler implements AudioSendHandler { |
339 | long counter = -1; |
340 | byte[] data; |
341 | |
342 | public bool canProvide() enter { |
343 | inc(canProvideCalls); |
344 | if (!shouldBePlaying) false; |
345 | if (shadowStopped()) { |
346 | counter = -1; |
347 | false; |
348 | } |
349 | |
350 | synchronized(buffer) { |
351 | if (!buffer.isFull()) false; |
352 | |
353 | // out of range? go back to middle |
354 | data = buffer.get(counter); |
355 | if (data == null) { |
356 | long oldCounter = counter; |
357 | counter = buffer.getBase()+buffer.size()/2; |
358 | if (oldCounter >= 0) printWithTime("RESETTING " + oldCounter + " => " + counter |
359 | + " (delta=" + (counter-oldCounter) + ")"); |
360 | data = buffer.get(counter); |
361 | } |
362 | ++counter; |
363 | true; |
364 | } |
365 | } |
366 | |
367 | public java.nio.ByteBuffer provide20MsAudio() { |
368 | inc(provideCalls); |
369 | inc(dataSent, l(data)); |
370 | java.nio.ByteBuffer buf = java.nio.ByteBuffer.wrap(data); |
371 | ret buf; |
372 | } |
373 | |
374 | public bool isOpus() { true; } |
375 | } |
376 | |
377 | // when already in channel |
378 | S startPlaying(Guild guild) { |
379 | AudioManager am = guild.getAudioManager(); |
380 | sendHandler = new MyAudioSendHandler; |
381 | am.setSendingHandler(sendHandler); |
382 | |
383 | ret "Playing radio in channel " + quote(voiceChannelName); |
384 | } |
385 | |
386 | // try to select the best channel |
387 | // -the one the user is in, or |
388 | // -the one that was used before, or |
389 | // -just any channel. |
390 | // returns error message or null |
391 | S selectAVoiceChannel(Guild guild, Member member) { |
392 | print("selectAVoiceChannel member=" + member); |
393 | if (member != null) { |
394 | GuildVoiceState vs = member.getVoiceState(); |
395 | VoiceChannel vc = vs.getChannel(); |
396 | print("selectAVoiceChannel vc=" + vc); |
397 | if (vc != null) { |
398 | voiceChannelID = lastVoiceChannelID = vc.getIdLong(); |
399 | ret null with change(); |
400 | } |
401 | } |
402 | |
403 | if (voiceChannelID != 0) null; |
404 | L<VoiceChannel> allChannels = guild.getVoiceChannels(); |
405 | L<VoiceChannel> channels = filter(allChannels, c -> isJoinableVoiceChannel(guild, c)); |
406 | |
407 | if (empty(channels)) |
408 | if (empty(allChannels)) |
409 | ret "Please create a voice channel."; |
410 | else |
411 | ret "Sorry, I am not allowed to enter any of your voice channels. Maybe my invite link was wrong?"; // TODO: show better link |
412 | |
413 | // Check if lastVoiceChannelID still exists |
414 | if (lastVoiceChannelID != 0 && !hasWhereMethodReturns getIdLong(channels, lastVoiceChannelID)) { |
415 | lastVoiceChannelID = 0; change(); |
416 | } |
417 | |
418 | voiceChannelID = lastVoiceChannelID != 0 ? lastVoiceChannelID |
419 | : first(channels).getIdLong(); |
420 | change(); |
421 | null; |
422 | } |
423 | |
424 | S action_play(Guild guild, Member member) { |
425 | if (shouldBePlaying && sendHandler != null) ret "I am playing already!"; |
426 | try answer selectAVoiceChannel(guild, member); |
427 | joinVoiceChannel(guild); |
428 | shouldBePlaying = true; change(); |
429 | ret startPlaying(guild); |
430 | } |
431 | |
432 | S action_stop(Guild guild) { |
433 | if (shouldBePlaying) { |
434 | shouldBePlaying = false; change(); |
435 | ret "OK, radio switched off"; |
436 | } |
437 | ret "I am stopped, I think. No?"; |
438 | } |
439 | |
440 | S action_leave(Guild guild) { |
441 | voiceChannelID = 0; change(); |
442 | this.vc = null; |
443 | GuildVoiceState state = getSelfMember(guild).getVoiceState(); |
444 | VoiceChannel vc = state.getChannel(); |
445 | if (vc == null) ret "I don't think I am in any voice channel"; |
446 | guild.getAudioManager().closeAudioConnection(); |
447 | ret "OK, leaving channel " + quote(vc.getName()); |
448 | } |
449 | |
450 | S action_join(Guild guild, Member member) { |
451 | GuildVoiceState state = getSelfMember(guild).getVoiceState(); |
452 | VoiceChannel vc = state.getChannel(); |
453 | try answer selectAVoiceChannel(guild, member); |
454 | joinVoiceChannel(guild); |
455 | ret "OK, joined voice channel " + quote(voiceChannelName); |
456 | } |
457 | |
458 | S action_selectChannel(Guild guild, int i) { |
459 | L<VoiceChannel> channels = guild.getVoiceChannels(); |
460 | channels = filter(channels, c -> isJoinableVoiceChannel(guild, c)); |
461 | VoiceChannel vc = _get(channels, i-1); |
462 | if (vc == null) ret "Sorry, I count only " + nChannels(channels); |
463 | voiceChannelID = lastVoiceChannelID = vc.getIdLong(); |
464 | change(); |
465 | joinVoiceChannel(guild); |
466 | ret "OK, joined voice channel " + quote(voiceChannelName); |
467 | } |
468 | |
469 | S renderChannelListIfNotOne(Guild guild) { |
470 | if (guild == null) ret ""; |
471 | L<VoiceChannel> channels = guild.getVoiceChannels(); |
472 | channels = filter(channels, c -> isJoinableVoiceChannel(guild, c)); |
473 | if (empty(channels)) ret "\nNote: You don't have any voice channels."; |
474 | if (l(channels) == 1) ret ""; |
475 | new LS out; |
476 | for i over channels: |
477 | out.add("`@me join channel " + (i+1) + "` - join channel " + quote(channels.get(i).getName())); |
478 | ret "\n" + lines_rtrim(out); |
479 | } |
480 | |
481 | bool isJoinableVoiceChannel(Guild guild, VoiceChannel vc) { |
482 | if (vc == null || guild == null) false; |
483 | try { |
484 | ret guild.getSelfMember().hasPermission(vc, Permission.VOICE_CONNECT); |
485 | } catch print e { ret false; } |
486 | } |
487 | |
488 | // normal restart (on bot load) |
489 | void restart(Guild guild) { |
490 | if (guild == null) ret; // no longer in guild |
491 | if (shouldBePlaying) { |
492 | print("RESTARTING RADIO in " + guild); |
493 | print(action_play(guild, null)); |
494 | } else if (voiceChannelID != 0) { |
495 | print("Rejoining voice channel in " + guild); |
496 | print(action_join(guild, null)); |
497 | } |
498 | } |
499 | |
500 | // restart with stop (after stream interruption) - no longer needed |
501 | /*void hardRestart(Guild guild) { |
502 | restart(guild); |
503 | }*/ |
504 | |
505 | void postSong { |
506 | if (songChannelID == 0) ret; |
507 | postInChannel(songChannelID, npMsg.replace("<track>", backtickQuote(currentSong))); |
508 | if (isWebURL(coverArtURL)) { |
509 | S imageURL = appendQueryToURL(coverArtURL, rand := randomInt()); |
510 | if (uploadImagesAsFiles) |
511 | uploadFileInChannel(songChannelID, loadBinaryPage(imageURL), "cover-art.jpg", "", null); |
512 | else |
513 | postImage(getChannel(songChannelID), imageURL, ""); |
514 | } |
515 | } |
516 | |
517 | void handleUserJoined(long userID, VoiceChannel joined) { |
518 | if (joined != null && joined.getIdLong() == voiceChannelID) { |
519 | usersInVoiceChannel.add(userID); |
520 | print(guildID + " adding user to voice channel: " + userID + " (n=" + l(usersInVoiceChannel)); |
521 | } |
522 | } |
523 | |
524 | void handleVoiceUpdateEvent(GenericGuildVoiceEvent e) { |
525 | User user = ((GenericGuildVoiceEvent) e).getMember().getUser(); |
526 | if (user.isBot()) ret; |
527 | long userID = user.getIdLong(); |
528 | if (e cast GuildVoiceUpdateEvent) { |
529 | VoiceChannel left = e.getChannelLeft(); |
530 | if (e.getChannelLeft().getIdLong() == voiceChannelID) { |
531 | usersInVoiceChannel.remove(userID); |
532 | print(guildID + " removed user from voice channel: " + userID + " (n=" + l(usersInVoiceChannel)); |
533 | } |
534 | } |
535 | if (e cast GuildVoiceMoveEvent) |
536 | handleUserJoined(userID, e.getChannelJoined()); |
537 | if (e cast GuildVoiceJoinEvent) |
538 | handleUserJoined(userID, e.getChannelJoined()); |
539 | } |
540 | } // end of ByServer |
541 | |
542 | /*void restartRadios { |
543 | L<Guild> guilds = discord.getGuilds(); |
544 | for (Guild guild : guilds) pcall { |
545 | ByServer bs = getByServer(guild); |
546 | if (bs != null) |
547 | bs.hardRestart(guild); |
548 | } |
549 | }*/ |
550 | |
551 | S dmOwners(S msg) { |
552 | L<Guild> guilds = discord.getGuilds(); |
553 | int n = 0; |
554 | for (Guild guild : guilds) pcall { |
555 | ByServer bs = getByServer(guild.getIdLong(), true); |
556 | if (bs != null && bs.shouldBePlaying) { |
557 | gazelle_dmGuildOwner(guild, msg); |
558 | ++n; |
559 | } |
560 | } |
561 | ret n2(n, "guild owner") + " contacted."; |
562 | } |
563 | |
564 | void loadTrack { |
565 | if (!isURL(radioURL)) ret; |
566 | lavaPlayer_loadItem(module(), lavaPlayer_playerManager(), player, radioURL.replace("<RNDINT>", str(randomInt()))); |
567 | } |
568 | |
569 | S currentSong() { |
570 | temp tempSetTL(loadPage_sizeLimit, 128*1024L); |
571 | if (l(currentSongURL) <= 1) ret ""; |
572 | if (cic(currentSongURL, "autopo.st/")) |
573 | ret trackTitleFromAutoPostWidget(loadPageWithTimeout(currentSongURL, 10.0)); |
574 | if (cic(currentSongURL, "caster.fm/")) |
575 | ret (S) mapGet(jsonDecodeMap(loadPageWithHeaders(currentSongURL, |
576 | "X-Requested-With", "XMLHttpRequest")), "playing"); |
577 | if (endsWith(currentSongURL, "status.xsl")) |
578 | ret trackTitleFromIceCastStatusXSL(loadPageWithTimeout(currentSongURL, 10.0)); |
579 | ret loadPageWithTimeout(currentSongURL, 10.0); |
580 | } |
581 | |
582 | void grabSong enter { |
583 | if (discord == null) ret; |
584 | S song = currentSong(); |
585 | bool post = nempty(currentSong) && nempty(song) && neq(currentSong, song); |
586 | currentSong = song; |
587 | if (post) |
588 | postSong_allServers(); |
589 | if (putSongInStatus) { |
590 | S s = currentSong; |
591 | if (elapsedSeconds(lastActualFrameReceived) >= considerDownInterval && elapsedSeconds(moduleStarted) >= considerDownInterval) |
592 | s = "Stream currently down"; |
593 | if (nempty(s)) |
594 | gazelle_discord_playingGame(s); |
595 | } |
596 | } |
597 | |
598 | void postSong_allServers { |
599 | L<Guild> guilds = discord.getGuilds(); |
600 | for (Guild guild : guilds) pcall { |
601 | ByServer bs = getByServer(guild); |
602 | if (bs != null) |
603 | bs.postSong(); |
604 | } |
605 | } |
606 | |
607 | void updateVoiceChannelCount_allServers { |
608 | if (discord == null) ret; |
609 | for (ByServer bs : cloneValues(dataByServer)) |
610 | bs.updateVoiceChannelCount(); |
611 | } |
612 | |
613 | int listeningUsers() { |
614 | int n = 0; |
615 | for (Guild guild : discord.getGuilds()) { |
616 | ByServer bs = getByServer(guild.getIdLong(), true); |
617 | if (bs != null && bs.shouldBePlaying) |
618 | n += l(bs.usersInVoiceChannel); |
619 | } |
620 | ret n; |
621 | } |
622 | |
623 | S listeningUsers_full() { |
624 | new LS out; |
625 | for (Guild guild : discord.getGuilds()) { |
626 | ByServer bs = getByServer(guild.getIdLong(), true); |
627 | if (bs != null && bs.shouldBePlaying) { |
628 | L<Long> users = cloneList(bs.usersInVoiceChannel); |
629 | if (empty(users)) continue; |
630 | out.add(guild.getName() + ": playing to " + joinWithComma(map(users, |
631 | id -> or2(discord_memberIDToEffectiveName(guild, id), str(id))))); |
632 | } |
633 | } |
634 | ret lines(out); |
635 | } |
636 | } |
Began life as a copy of #1030356
download show line numbers debug dex old transpilations
Travelled to 2 computer(s): bhatertpkbcr, mqqgnosmbjvj
No comments. add comment
Snippet ID: | #1034055 |
Snippet name: | MKLab FM Bot with JDA 4.2 [using sharded classes] |
Eternal ID of this version: | #1034055/1 |
Text MD5: | 514ccbc82ecb6fedfbde596d2596b4ac |
Author: | stefan |
Category: | javax |
Type: | JavaX source code (Dynamic Module) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-01-19 17:08:06 |
Source code size: | 22867 bytes / 636 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 104 / 123 |
Referenced in: | [show references] |