mirror of
https://github.com/chrisb09/MusicBot.git
synced 2026-11-05 17:02:35 -05:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c25cda721a | ||
|
|
f95b50af89 | ||
|
|
1e6904d329 |
No files matched your search
@@ -72,6 +72,8 @@ dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
|
||||
|
||||
make sure the database file lives somewhere inside your mounted persistent config or data path if you want it to survive container recreation.
|
||||
|
||||
The `stats` command and scheduled server rewind embeds read from this database, so reports only include activity logged after analytics were enabled.
|
||||
|
||||
## YouTube Token Notes In Docker
|
||||
|
||||
If you use YouTube login or PO token based configuration, keep those values in your persistent mounted config rather than baking them into the image.
|
||||
|
||||
@@ -148,6 +148,21 @@ dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
|
||||
|
||||
This creates a local SQLite database file and records queue, playback, listener, and command activity.
|
||||
|
||||
When analytics are enabled, the `stats` command can show server, personal, track, source, and skip reports:
|
||||
|
||||
```text
|
||||
stats server month
|
||||
stats me week source:youtube
|
||||
stats track
|
||||
stats tracks 60d limit:20 userlist:[alice,bob]
|
||||
stats listened 1y users:alice
|
||||
stats reports set #music-stats monthly
|
||||
stats reports now 2026-04
|
||||
stats reports now 2025
|
||||
```
|
||||
|
||||
Personal member reports are self-only unless the requester has Manage Server. Scheduled reports are Discord embeds; graphic rewind images are not generated yet.
|
||||
|
||||
## Windows Note
|
||||
|
||||
If you run the bot directly on Windows rather than in Docker, install Java first and consider a service wrapper such as [NSSM](http://nssm.cc/) if you want it to run in the background.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.jagrosh</groupId>
|
||||
<artifactId>JMusicBot</artifactId>
|
||||
<version>0.6.3</version>
|
||||
<version>0.6.6</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<repositories>
|
||||
@@ -94,25 +94,25 @@
|
||||
<dependency>
|
||||
<groupId>pw.chew</groupId>
|
||||
<artifactId>jda-chewtils-command</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>${jda.chewtils.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>pw.chew</groupId>
|
||||
<artifactId>jda-chewtils-commons</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>${jda.chewtils.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>pw.chew</groupId>
|
||||
<artifactId>jda-chewtils-examples</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>${jda.chewtils.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>pw.chew</groupId>
|
||||
<artifactId>jda-chewtils-menu</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>${jda.chewtils.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<!-- <dependency>
|
||||
@@ -161,7 +161,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.lavalink.youtube</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>1.16.0</version>
|
||||
<version>1.18.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jagrosh</groupId>
|
||||
@@ -244,6 +244,16 @@
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/filtered-resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
@@ -287,6 +297,7 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jda.chewtils.version>2.2.1</jda.chewtils.version>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
jda-chewtils.version=${jda.chewtils.version}
|
||||
@@ -24,6 +24,7 @@ import com.jagrosh.jmusicbot.audio.NowplayingHandler;
|
||||
import com.jagrosh.jmusicbot.audio.PlayerManager;
|
||||
import com.jagrosh.jmusicbot.audio.StatusMessageHandler;
|
||||
import com.jagrosh.jmusicbot.datalog.DataLogService;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsReportScheduler;
|
||||
import com.jagrosh.jmusicbot.gui.GUI;
|
||||
import com.jagrosh.jmusicbot.playlist.PlaylistLoader;
|
||||
import com.jagrosh.jmusicbot.settings.SettingsManager;
|
||||
@@ -49,6 +50,7 @@ public class Bot
|
||||
private final StatusMessageHandler statusMessageHandler;
|
||||
private final AloneInVoiceHandler aloneInVoiceHandler;
|
||||
private final DataLogService dataLogService;
|
||||
private final StatsReportScheduler statsReportScheduler;
|
||||
private final YoutubeOauth2TokenHandler youTubeOauth2TokenHandler;
|
||||
|
||||
private boolean shuttingDown = false;
|
||||
@@ -85,6 +87,8 @@ public class Bot
|
||||
}
|
||||
}
|
||||
this.dataLogService = dls;
|
||||
this.statsReportScheduler = new StatsReportScheduler(this);
|
||||
this.statsReportScheduler.init();
|
||||
}
|
||||
|
||||
public BotConfig getConfig()
|
||||
@@ -137,6 +141,11 @@ public class Bot
|
||||
return dataLogService;
|
||||
}
|
||||
|
||||
public StatsReportScheduler getStatsReportScheduler()
|
||||
{
|
||||
return statsReportScheduler;
|
||||
}
|
||||
|
||||
public YoutubeOauth2TokenHandler getYouTubeOauth2Handler()
|
||||
{
|
||||
return youTubeOauth2TokenHandler;
|
||||
|
||||
@@ -196,7 +196,7 @@ public class JMusicBot
|
||||
{
|
||||
// instantiate about command
|
||||
LoggedAboutCmd aboutCommand = new LoggedAboutCmd(bot, Color.BLUE.brighter(),
|
||||
"a music bot that is [easy to host yourself!](https://github.com/jagrosh/MusicBot) (v" + OtherUtil.getCurrentVersion() + ")",
|
||||
"a music bot that is [easy to host yourself!](https://github.com/chrisb09/MusicBot) (v" + OtherUtil.getCurrentVersion() + ")",
|
||||
new String[]{"High-quality music playback", "FairQueue™ Technology", "Easy to host yourself"},
|
||||
RECOMMENDED_PERMS);
|
||||
aboutCommand.setIsAuthor(false);
|
||||
@@ -214,6 +214,7 @@ public class JMusicBot
|
||||
.addCommands(aboutCommand,
|
||||
new LoggedPingCmd(bot),
|
||||
new SettingsCmd(bot),
|
||||
new StatsCmd(bot),
|
||||
|
||||
new LyricsCmd(bot),
|
||||
new NowplayingCmd(bot),
|
||||
|
||||
@@ -65,6 +65,8 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
|
||||
|
||||
private AudioFrame lastFrame;
|
||||
private AbstractQueue<QueuedTrack> queue;
|
||||
private volatile String playCountTrackIdentifier;
|
||||
private volatile long currentTrackServerPlayCount = -1L;
|
||||
|
||||
protected AudioHandler(PlayerManager manager, Guild guild, AudioPlayer player)
|
||||
{
|
||||
@@ -239,6 +241,13 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
|
||||
String searchQuery = rm.queueInfo == null ? null : rm.queueInfo.searchQuery;
|
||||
String playlistName = rm.queueInfo == null ? null : rm.queueInfo.playlistName;
|
||||
manager.getBot().getDataLogService().logPlayStart(guild(manager.getBot().getJDA()), track, rm, source, searchQuery, playlistName);
|
||||
playCountTrackIdentifier = track.getIdentifier();
|
||||
currentTrackServerPlayCount = manager.getBot().getDataLogService().getGuildTrackPlayCount(guild(manager.getBot().getJDA()), track);
|
||||
}
|
||||
else
|
||||
{
|
||||
playCountTrackIdentifier = null;
|
||||
currentTrackServerPlayCount = -1L;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +295,8 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
|
||||
+ " "+FormatUtil.progressBar(progress)
|
||||
+ " `[" + TimeUtil.formatTime(track.getPosition()) + "/" + TimeUtil.formatTime(track.getDuration()) + "]` "
|
||||
+ FormatUtil.volumeIcon(audioPlayer.getVolume()));
|
||||
if(currentTrackServerPlayCount > 0L && track.getIdentifier().equals(playCountTrackIdentifier))
|
||||
eb.addField("Server plays", "Played " + currentTrackServerPlayCount + " time" + (currentTrackServerPlayCount == 1L ? "" : "s") + " on this server", false);
|
||||
|
||||
return mb.setEmbeds(eb.build()).build();
|
||||
}
|
||||
@@ -341,6 +352,9 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
|
||||
.append(" `[").append(TimeUtil.formatTime(track.getPosition()))
|
||||
.append("/").append(TimeUtil.formatTime(track.getDuration())).append("]` ")
|
||||
.append(FormatUtil.volumeIcon(audioPlayer.getVolume()));
|
||||
if(currentTrackServerPlayCount > 0L && track.getIdentifier().equals(playCountTrackIdentifier))
|
||||
desc.append("\nPlayed ").append(currentTrackServerPlayCount).append(" time")
|
||||
.append(currentTrackServerPlayCount == 1L ? "" : "s").append(" on this server");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -18,28 +18,184 @@ package com.jagrosh.jmusicbot.commands.general;
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jdautilities.examples.command.AboutCommand;
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.jagrosh.jmusicbot.JMusicBot;
|
||||
import com.jagrosh.jmusicbot.utils.OtherUtil;
|
||||
import java.awt.Color;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDAInfo;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.ApplicationInfo;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class LoggedAboutCmd extends AboutCommand
|
||||
{
|
||||
private final Bot bot;
|
||||
private final Color color;
|
||||
private final String description;
|
||||
private final String[] features;
|
||||
private final Permission[] perms;
|
||||
private boolean isAuthor = true;
|
||||
private String replacementCharacter = "+";
|
||||
private String oauthLink;
|
||||
|
||||
public LoggedAboutCmd(Bot bot, Color color, String description, String[] features, net.dv8tion.jda.api.Permission[] perms)
|
||||
public LoggedAboutCmd(Bot bot, Color color, String description, String[] features, Permission[] perms)
|
||||
{
|
||||
super(color, description, features, perms);
|
||||
this.bot = bot;
|
||||
this.color = color;
|
||||
this.description = description;
|
||||
this.features = features == null ? new String[0] : features.clone();
|
||||
this.perms = perms == null ? new Permission[0] : perms.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIsAuthor(boolean isAuthor)
|
||||
{
|
||||
super.setIsAuthor(isAuthor);
|
||||
this.isAuthor = isAuthor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReplacementCharacter(String replacementCharacter)
|
||||
{
|
||||
super.setReplacementCharacter(replacementCharacter);
|
||||
this.replacementCharacter = replacementCharacter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(CommandEvent event)
|
||||
{
|
||||
super.execute(event);
|
||||
if(oauthLink == null)
|
||||
oauthLink = resolveOauthLink(event);
|
||||
|
||||
EmbedBuilder builder = new EmbedBuilder()
|
||||
.setColor(event.isFromType(ChannelType.TEXT)
|
||||
? event.getGuild().getSelfMember().getColor()
|
||||
: color)
|
||||
.setAuthor("All about " + event.getSelfUser().getName() + "!",
|
||||
null,
|
||||
event.getSelfUser().getAvatarUrl())
|
||||
.setDescription(buildDescription(event));
|
||||
|
||||
addStats(builder, event);
|
||||
builder.setFooter("Last restart", null)
|
||||
.setTimestamp(event.getClient().getStartTime());
|
||||
|
||||
event.reply(builder.build());
|
||||
if(bot.getDataLogService() != null && event.getGuild() != null)
|
||||
{
|
||||
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
|
||||
event.getArgs(), "OK", new JSONObject().put("about", true).toString());
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveOauthLink(CommandEvent event)
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplicationInfo info = event.getJDA().retrieveApplicationInfo().complete();
|
||||
return info.isBotPublic() ? info.getInviteUrl(0, perms) : "";
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
JMusicBot.LOG.error("Could not generate invite link", ex);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String buildDescription(CommandEvent event)
|
||||
{
|
||||
String owner = getOwnerDisplay(event);
|
||||
String links = getLinksSection(event);
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append("Hello! I am **").append(event.getSelfUser().getName()).append("**, ")
|
||||
.append(description)
|
||||
.append("\nI ")
|
||||
.append(isAuthor ? "was written in Java" : "am owned")
|
||||
.append(" by **").append(owner).append("**")
|
||||
.append(" using Chew's [Commands Extension](https://github.com/Chew/JDA-Chewtils) (")
|
||||
.append(OtherUtil.getCommandLibraryVersion())
|
||||
.append(") and the [JDA library](https://github.com/DV8FromTheWorld/JDA) (")
|
||||
.append(JDAInfo.VERSION)
|
||||
.append(")\nType `")
|
||||
.append(event.getClient().getTextualPrefix())
|
||||
.append(event.getClient().getHelpWord())
|
||||
.append("` to see my commands!");
|
||||
|
||||
if(!links.isEmpty())
|
||||
builder.append(links);
|
||||
|
||||
builder.append("\n\nSome of my features include: ```css");
|
||||
for(String feature : features)
|
||||
{
|
||||
builder.append("\n")
|
||||
.append(event.getClient().getSuccess().startsWith("<")
|
||||
? replacementCharacter
|
||||
: event.getClient().getSuccess())
|
||||
.append(" ")
|
||||
.append(feature);
|
||||
}
|
||||
builder.append("```");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String getOwnerDisplay(CommandEvent event)
|
||||
{
|
||||
User owner = event.getJDA().getUserById(event.getClient().getOwnerId());
|
||||
return owner == null ? "<@" + event.getClient().getOwnerId() + ">" : owner.getName();
|
||||
}
|
||||
|
||||
private String getLinksSection(CommandEvent event)
|
||||
{
|
||||
String serverInvite = event.getClient().getServerInvite();
|
||||
boolean hasServerInvite = serverInvite != null && !serverInvite.isEmpty();
|
||||
boolean hasOauthLink = oauthLink != null && !oauthLink.isEmpty();
|
||||
if(!hasServerInvite && !hasOauthLink)
|
||||
return "";
|
||||
|
||||
StringBuilder builder = new StringBuilder("\n");
|
||||
if(hasServerInvite)
|
||||
builder.append("Join my server [`here`](").append(serverInvite).append(")");
|
||||
else if(hasOauthLink)
|
||||
builder.append("Please");
|
||||
|
||||
if(hasOauthLink)
|
||||
{
|
||||
if(hasServerInvite)
|
||||
builder.append(", or");
|
||||
builder.append(" [`invite`](").append(oauthLink).append(") me to your server");
|
||||
}
|
||||
builder.append("!");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void addStats(EmbedBuilder builder, CommandEvent event)
|
||||
{
|
||||
JDA jda = event.getJDA();
|
||||
if(jda.getShardInfo() == JDA.ShardInfo.SINGLE)
|
||||
{
|
||||
builder.addField("Stats", jda.getGuilds().size() + " servers\n1 shard", true)
|
||||
.addField("Users",
|
||||
jda.getUsers().size() + " unique\n"
|
||||
+ jda.getGuilds().stream().mapToInt(Guild::getMemberCount).sum() + " total",
|
||||
true)
|
||||
.addField("Channels",
|
||||
jda.getTextChannels().size() + " Text\n" + jda.getVoiceChannels().size() + " Voice",
|
||||
true);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.addField("Stats",
|
||||
event.getClient().getTotalGuilds() + " Servers\nShard "
|
||||
+ (jda.getShardInfo().getShardId() + 1) + "/" + jda.getShardInfo().getShardTotal(),
|
||||
true)
|
||||
.addField("This shard", jda.getUsers().size() + " Users\n" + jda.getGuilds().size() + " Servers", true)
|
||||
.addField("",
|
||||
jda.getTextChannels().size() + " Text Channels\n" + jda.getVoiceChannels().size() + " Voice Channels",
|
||||
true);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,9 @@ public class SettingsCmd extends LoggedCommand
|
||||
.put("has_custom_prefix", s.getPrefix() != null)
|
||||
.put("has_text_channel", s.getTextChannel(event.getGuild()) != null)
|
||||
.put("has_voice_channel", s.getVoiceChannel(event.getGuild()) != null)
|
||||
.put("has_dj_role", s.getRole(event.getGuild()) != null);
|
||||
.put("has_dj_role", s.getRole(event.getGuild()) != null)
|
||||
.put("stats_report_frequency", s.getStatsReportFrequency().name())
|
||||
.put("has_stats_report_channel", s.getStatsReportChannel(event.getGuild()) != null);
|
||||
CommandLogContext.setMeta(meta);
|
||||
MessageCreateBuilder builder = new MessageCreateBuilder()
|
||||
.addContent(EMOJI + " **")
|
||||
@@ -66,6 +68,7 @@ public class SettingsCmd extends LoggedCommand
|
||||
.addContent("** settings:");
|
||||
TextChannel tchan = s.getTextChannel(event.getGuild());
|
||||
VoiceChannel vchan = s.getVoiceChannel(event.getGuild());
|
||||
TextChannel statsChannel = s.getStatsReportChannel(event.getGuild());
|
||||
Role role = s.getRole(event.getGuild());
|
||||
EmbedBuilder ebuilder = new EmbedBuilder()
|
||||
.setColor(event.getSelfMember().getColor())
|
||||
@@ -80,6 +83,8 @@ public class SettingsCmd extends LoggedCommand
|
||||
? s.getQueueType().getUserFriendlyName()
|
||||
: "**"+s.getQueueType().getUserFriendlyName()+"**")
|
||||
+ "\nDefault Playlist: " + (s.getDefaultPlaylist() == null ? "None" : "**" + s.getDefaultPlaylist() + "**")
|
||||
+ "\nStats Reports: " + (s.getStatsReportFrequency().name())
|
||||
+ (statsChannel == null ? "" : " in " + statsChannel.getAsMention())
|
||||
)
|
||||
.setFooter(event.getJDA().getGuilds().size() + " servers | "
|
||||
+ event.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inAudioChannel()).count()
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.commands.general;
|
||||
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import com.jagrosh.jmusicbot.audio.AudioHandler;
|
||||
import com.jagrosh.jmusicbot.commands.LoggedCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsFilters;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsAccess;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsMessageFormatter;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsSummary;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsTimeRange;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsRow;
|
||||
import com.jagrosh.jmusicbot.settings.Settings;
|
||||
import com.jagrosh.jmusicbot.settings.StatsReportFrequency;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class StatsCmd extends LoggedCommand
|
||||
{
|
||||
public StatsCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "stats";
|
||||
this.help = "shows playback statistics";
|
||||
this.arguments = "<server|me|user|tracks|listened|track|skips|sources|reports>";
|
||||
this.guildOnly = true;
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
if(bot.getDataLogService() == null)
|
||||
{
|
||||
event.replyWarning("Playback analytics are not enabled.");
|
||||
CommandLogContext.setError("analytics_disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> tokens = new ArrayList<>(Arrays.asList(event.getArgs().trim().split("\\s+")));
|
||||
if(tokens.size() == 1 && tokens.get(0).isEmpty())
|
||||
tokens.clear();
|
||||
String subcommand = tokens.isEmpty() ? "server" : tokens.remove(0).toLowerCase();
|
||||
switch(subcommand)
|
||||
{
|
||||
case "server":
|
||||
sendSummary(event, "Server stats", parseOptions(tokens), null, Section.ALL);
|
||||
break;
|
||||
case "me":
|
||||
sendSummary(event, "Your stats", parseOptions(tokens), event.getAuthor().getIdLong(), Section.ALL);
|
||||
break;
|
||||
case "user":
|
||||
handleUserStats(event, tokens);
|
||||
break;
|
||||
case "track":
|
||||
handleTrackStats(event, parseOptions(tokens));
|
||||
break;
|
||||
case "tracks":
|
||||
case "played":
|
||||
handleTrackListStats(event, "Top played tracks", parseOptions(tokens), false);
|
||||
break;
|
||||
case "listened":
|
||||
case "listens":
|
||||
handleTrackListStats(event, "Top listened tracks", parseOptions(tokens), true);
|
||||
break;
|
||||
case "skips":
|
||||
sendSummary(event, "Skip stats", parseOptions(tokens), null, Section.SKIPS);
|
||||
break;
|
||||
case "sources":
|
||||
sendSummary(event, "Source stats", parseOptions(tokens), null, Section.SOURCES);
|
||||
break;
|
||||
case "reports":
|
||||
handleReports(event, tokens);
|
||||
break;
|
||||
case "help":
|
||||
sendUsage(event);
|
||||
break;
|
||||
default:
|
||||
tokens.add(0, subcommand);
|
||||
handleUserStats(event, tokens);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUserStats(CommandEvent event, List<String> tokens)
|
||||
{
|
||||
Options options = parseOptions(tokens);
|
||||
String targetText = String.join(" ", options.remaining).trim();
|
||||
if(targetText.isEmpty())
|
||||
{
|
||||
event.replyError("Please include a member.");
|
||||
CommandLogContext.setError("missing_member");
|
||||
return;
|
||||
}
|
||||
Long targetId = parseUserId(targetText);
|
||||
if(targetId != null)
|
||||
{
|
||||
Member member = event.getGuild().getMemberById(targetId);
|
||||
if(!StatsAccess.canViewUserStats(event.getAuthor().getIdLong(), targetId, canManageServer(event.getMember())))
|
||||
{
|
||||
event.replyError("You can only view another member's detailed stats if you can manage this server.");
|
||||
CommandLogContext.setError("missing_manage_server");
|
||||
return;
|
||||
}
|
||||
sendSummary(event, member == null ? "Stats for user " + targetId : "Stats for " + member.getEffectiveName(), options, targetId, Section.ALL);
|
||||
return;
|
||||
}
|
||||
List<Member> members = FinderUtil.findMembers(targetText, event.getGuild());
|
||||
if(members.isEmpty())
|
||||
members = findMembersByPartialName(targetText, event.getGuild().getMembers());
|
||||
if(members.isEmpty())
|
||||
{
|
||||
List<StatsRow> profileMatches = bot.getDataLogService().findUsersByProfile(event.getGuild().getIdLong(), targetText, 6);
|
||||
if(profileMatches.size() == 1)
|
||||
{
|
||||
StatsRow match = profileMatches.get(0);
|
||||
if(!StatsAccess.canViewUserStats(event.getAuthor().getIdLong(), match.getId(), canManageServer(event.getMember())))
|
||||
{
|
||||
event.replyError("You can only view another member's detailed stats if you can manage this server.");
|
||||
CommandLogContext.setError("missing_manage_server");
|
||||
return;
|
||||
}
|
||||
sendSummary(event, "Stats for " + match.getLabel(), options, match.getId(), Section.ALL);
|
||||
return;
|
||||
}
|
||||
if(profileMatches.size() > 1)
|
||||
{
|
||||
event.replyWarning("Multiple analytics users matched `" + targetText + "`: " + formatProfileMatches(profileMatches) + ". Please be more specific or use a mention.");
|
||||
CommandLogContext.setError("member_ambiguous");
|
||||
return;
|
||||
}
|
||||
event.replyWarning("No member found matching `" + targetText + "`. Try a mention like `@user` or the Discord user ID.");
|
||||
CommandLogContext.setError("member_not_found");
|
||||
return;
|
||||
}
|
||||
if(members.size() > 1)
|
||||
{
|
||||
event.replyWarning("Multiple members matched `" + targetText + "`: " + formatMemberMatches(members) + ". Please be more specific or use a mention.");
|
||||
CommandLogContext.setError("member_ambiguous");
|
||||
return;
|
||||
}
|
||||
Member target = members.get(0);
|
||||
if(!StatsAccess.canViewUserStats(event.getAuthor().getIdLong(), target.getIdLong(), canManageServer(event.getMember())))
|
||||
{
|
||||
event.replyError("You can only view another member's detailed stats if you can manage this server.");
|
||||
CommandLogContext.setError("missing_manage_server");
|
||||
return;
|
||||
}
|
||||
sendSummary(event, "Stats for " + target.getEffectiveName(), options, target.getIdLong(), Section.ALL);
|
||||
}
|
||||
|
||||
private void sendUsage(CommandEvent event)
|
||||
{
|
||||
event.reply("Use `stats server [range] [source:youtube]`, `stats me [range]`, `stats user <@user|user id|name> [range]`, `stats tracks [range] [limit:20] [userlist:[name,id]]`, `stats listened [range] [limit:20] [userlist:[name,id]]`, `stats track [range]`, `stats skips`, `stats sources`, or `stats reports`.");
|
||||
}
|
||||
|
||||
private Long parseUserId(String value)
|
||||
{
|
||||
if(value == null)
|
||||
return null;
|
||||
String cleaned = value.trim();
|
||||
if(cleaned.startsWith("<@") && cleaned.endsWith(">"))
|
||||
{
|
||||
cleaned = cleaned.substring(2, cleaned.length() - 1);
|
||||
if(cleaned.startsWith("!"))
|
||||
cleaned = cleaned.substring(1);
|
||||
}
|
||||
if(!cleaned.matches("\\d{15,25}"))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
return Long.parseLong(cleaned);
|
||||
}
|
||||
catch(NumberFormatException ex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Member> findMembersByPartialName(String query, List<Member> members)
|
||||
{
|
||||
String normalizedQuery = normalizeName(query);
|
||||
List<Member> matches = new ArrayList<>();
|
||||
if(normalizedQuery.isEmpty())
|
||||
return matches;
|
||||
for(Member member : members)
|
||||
{
|
||||
if(member.getUser().isBot())
|
||||
continue;
|
||||
if(nameMatches(normalizedQuery, member.getEffectiveName())
|
||||
|| nameMatches(normalizedQuery, member.getUser().getName())
|
||||
|| nameMatches(normalizedQuery, member.getNickname()))
|
||||
{
|
||||
matches.add(member);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private boolean nameMatches(String normalizedQuery, String name)
|
||||
{
|
||||
if(name == null || name.isEmpty())
|
||||
return false;
|
||||
return normalizeName(name).contains(normalizedQuery);
|
||||
}
|
||||
|
||||
private String normalizeName(String value)
|
||||
{
|
||||
return value == null ? "" : value.toLowerCase().replaceAll("[^a-z0-9]", "");
|
||||
}
|
||||
|
||||
private String formatMemberMatches(List<Member> members)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int max = Math.min(5, members.size());
|
||||
for(int i = 0; i < max; i++)
|
||||
{
|
||||
if(i > 0)
|
||||
builder.append(", ");
|
||||
Member member = members.get(i);
|
||||
builder.append("`").append(member.getEffectiveName()).append("`");
|
||||
}
|
||||
if(members.size() > max)
|
||||
builder.append(", and ").append(members.size() - max).append(" more");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String formatProfileMatches(List<StatsRow> rows)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int max = Math.min(5, rows.size());
|
||||
for(int i = 0; i < max; i++)
|
||||
{
|
||||
if(i > 0)
|
||||
builder.append(", ");
|
||||
builder.append("`").append(rows.get(i).getLabel()).append("`");
|
||||
}
|
||||
if(rows.size() > max)
|
||||
builder.append(", and ").append(rows.size() - max).append(" more");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void handleTrackStats(CommandEvent event, Options options)
|
||||
{
|
||||
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
AudioTrack track = handler == null ? null : handler.getPlayer().getPlayingTrack();
|
||||
if(track == null)
|
||||
{
|
||||
event.replyWarning("No track is currently playing.");
|
||||
CommandLogContext.setError("no_current_track");
|
||||
return;
|
||||
}
|
||||
long count = bot.getDataLogService().getGuildTrackPlayCount(event.getGuild(), track, options.range);
|
||||
EmbedBuilder eb = new EmbedBuilder()
|
||||
.setColor(event.getSelfMember().getColor())
|
||||
.setTitle(track.getInfo().title, track.getInfo().uri)
|
||||
.setDescription("Range: **" + options.range.getLabel() + "**\nPlayed **" + count + "** time" + (count == 1L ? "" : "s") + " on this server.");
|
||||
if(track.getInfo().author != null && !track.getInfo().author.isEmpty())
|
||||
eb.setFooter("Source: " + track.getInfo().author, null);
|
||||
event.getChannel().sendMessageEmbeds(eb.build()).queue();
|
||||
CommandLogContext.setMeta(new JSONObject().put("subcommand", "track").put("play_count", count));
|
||||
}
|
||||
|
||||
private void handleTrackListStats(CommandEvent event, String title, Options options, boolean listened)
|
||||
{
|
||||
List<Long> userIds = resolveUserList(event, options.userQueries);
|
||||
if(userIds == null)
|
||||
return;
|
||||
if(!userIds.isEmpty() && !canViewUserList(event, userIds))
|
||||
{
|
||||
event.replyError("You can only filter by other members if you can manage this server.");
|
||||
CommandLogContext.setError("missing_manage_server");
|
||||
return;
|
||||
}
|
||||
List<StatsRow> rows = listened
|
||||
? bot.getDataLogService().getTopListenedTracks(event.getGuild().getIdLong(), options.range, options.source, userIds, options.limit)
|
||||
: bot.getDataLogService().getTopRequestedTracks(event.getGuild().getIdLong(), options.range, options.source, userIds, options.limit);
|
||||
if(rows.isEmpty())
|
||||
{
|
||||
event.replyWarning("No playback analytics found for that report yet.");
|
||||
CommandLogContext.setError("no_data");
|
||||
return;
|
||||
}
|
||||
String description = "Range: **" + options.range.getLabel() + "**"
|
||||
+ (options.source == null ? "" : "\nSource: **" + options.source + "**")
|
||||
+ (userIds.isEmpty() ? "\nUsers: **all**" : "\nUsers: **" + userIds.size() + " selected**");
|
||||
EmbedBuilder eb = new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setColor(event.getSelfMember().getColorRaw())
|
||||
.setDescription(description);
|
||||
StatsMessageFormatter.addChunkedField(eb,
|
||||
listened ? "Tracks by listening time" : "Tracks by plays",
|
||||
listened
|
||||
? StatsMessageFormatter.trackListenList(rows, options.limit)
|
||||
: StatsMessageFormatter.countList(rows, "plays", options.limit),
|
||||
false);
|
||||
event.getChannel().sendMessageEmbeds(eb.build()).queue();
|
||||
CommandLogContext.setMeta(new JSONObject().put("range", options.range.getKey())
|
||||
.put("source", options.source == null ? JSONObject.NULL : options.source)
|
||||
.put("user_count", userIds.size())
|
||||
.put("limit", options.limit)
|
||||
.put("listened", listened));
|
||||
}
|
||||
|
||||
private void handleReports(CommandEvent event, List<String> tokens)
|
||||
{
|
||||
if(tokens.isEmpty())
|
||||
{
|
||||
event.reply("Use `stats reports set <channel> <monthly|yearly|off>` or `stats reports now [month|year|YYYY-MM|MM/YYYY|YYYY]`.");
|
||||
return;
|
||||
}
|
||||
String action = tokens.remove(0).toLowerCase();
|
||||
if("now".equals(action))
|
||||
{
|
||||
if(!canManageServer(event.getMember()))
|
||||
{
|
||||
event.replyError("You need Manage Server to send stats reports.");
|
||||
CommandLogContext.setError("missing_manage_server");
|
||||
return;
|
||||
}
|
||||
StatsTimeRange range = parseReportRange(tokens);
|
||||
if(range == null)
|
||||
{
|
||||
event.replyError("Use `stats reports now month`, `stats reports now year`, `stats reports now 2026-04`, `stats reports now 04/2026`, or `stats reports now 2025`.");
|
||||
CommandLogContext.setError("invalid_report_range");
|
||||
return;
|
||||
}
|
||||
boolean sent = bot.getStatsReportScheduler().sendReport(event.getGuild(), event.getTextChannel(), range);
|
||||
if(sent)
|
||||
{
|
||||
event.replySuccess("Stats report generated for `" + range.getLabel() + "`.");
|
||||
CommandLogContext.setMeta(new JSONObject().put("subcommand", "reports_now").put("range", range.getKey()));
|
||||
}
|
||||
else
|
||||
{
|
||||
event.replyWarning("No playback analytics found for that report yet.");
|
||||
CommandLogContext.setError("no_data");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!"set".equals(action))
|
||||
{
|
||||
event.replyWarning("Unknown reports action. Use `set` or `now`.");
|
||||
CommandLogContext.setError("unknown_reports_action");
|
||||
return;
|
||||
}
|
||||
if(!canManageServer(event.getMember()))
|
||||
{
|
||||
event.replyError("You need Manage Server to configure stats reports.");
|
||||
CommandLogContext.setError("missing_manage_server");
|
||||
return;
|
||||
}
|
||||
if(tokens.isEmpty())
|
||||
{
|
||||
event.replyError("Please include a channel and frequency, or `off`.");
|
||||
CommandLogContext.setError("missing_report_settings");
|
||||
return;
|
||||
}
|
||||
Settings settings = event.getClient().getSettingsFor(event.getGuild());
|
||||
if(tokens.size() == 1 && "off".equalsIgnoreCase(tokens.get(0)))
|
||||
{
|
||||
settings.setStatsReportFrequency(StatsReportFrequency.OFF);
|
||||
settings.setStatsReportChannel(null);
|
||||
event.replySuccess("Stats reports are now disabled.");
|
||||
CommandLogContext.setMeta(new JSONObject().put("subcommand", "reports_set").put("frequency", "OFF"));
|
||||
return;
|
||||
}
|
||||
if(tokens.size() < 2)
|
||||
{
|
||||
event.replyError("Use `stats reports set <channel> <monthly|yearly|off>`.");
|
||||
CommandLogContext.setError("missing_report_frequency");
|
||||
return;
|
||||
}
|
||||
String frequencyToken = tokens.remove(tokens.size() - 1);
|
||||
StatsReportFrequency frequency = StatsReportFrequency.parse(frequencyToken);
|
||||
if(frequency == StatsReportFrequency.OFF)
|
||||
{
|
||||
settings.setStatsReportFrequency(StatsReportFrequency.OFF);
|
||||
settings.setStatsReportChannel(null);
|
||||
event.replySuccess("Stats reports are now disabled.");
|
||||
CommandLogContext.setMeta(new JSONObject().put("subcommand", "reports_set").put("frequency", "OFF"));
|
||||
return;
|
||||
}
|
||||
String channelText = String.join(" ", tokens);
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
List<TextChannel> channels = (List)FinderUtil.findTextChannels(channelText, event.getGuild());
|
||||
if(channels.isEmpty())
|
||||
{
|
||||
event.replyWarning("No text channel found matching `" + channelText + "`.");
|
||||
CommandLogContext.setError("text_channel_not_found");
|
||||
return;
|
||||
}
|
||||
if(channels.size() > 1)
|
||||
{
|
||||
event.replyWarning("Multiple text channels matched `" + channelText + "`. Please be more specific.");
|
||||
CommandLogContext.setError("text_channel_ambiguous");
|
||||
return;
|
||||
}
|
||||
settings.setStatsReportChannel(channels.get(0));
|
||||
settings.setStatsReportFrequency(frequency);
|
||||
event.replySuccess("Stats reports will be posted " + frequency.name().toLowerCase() + " in " + channels.get(0).getAsMention() + ".");
|
||||
CommandLogContext.setMeta(new JSONObject().put("subcommand", "reports_set")
|
||||
.put("channel_id", channels.get(0).getId()).put("frequency", frequency.name()));
|
||||
}
|
||||
|
||||
private StatsTimeRange parseReportRange(List<String> tokens)
|
||||
{
|
||||
if(tokens.isEmpty())
|
||||
return StatsTimeRange.previousMonth();
|
||||
String value = String.join(" ", tokens).trim().toLowerCase();
|
||||
if(value.isEmpty() || "month".equals(value) || "monthly".equals(value))
|
||||
return StatsTimeRange.previousMonth();
|
||||
if("year".equals(value) || "yearly".equals(value))
|
||||
return StatsTimeRange.previousYear();
|
||||
if(value.matches("\\d{4}"))
|
||||
return StatsTimeRange.year(Integer.parseInt(value));
|
||||
if(value.matches("\\d{4}-\\d{1,2}"))
|
||||
{
|
||||
String[] parts = value.split("-");
|
||||
return StatsTimeRange.month(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||
}
|
||||
if(value.matches("\\d{1,2}/\\d{4}"))
|
||||
{
|
||||
String[] parts = value.split("/");
|
||||
return StatsTimeRange.month(Integer.parseInt(parts[1]), Integer.parseInt(parts[0]));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void sendSummary(CommandEvent event, String title, Options options, Long userId, Section section)
|
||||
{
|
||||
StatsSummary summary = bot.getDataLogService().getStatsSummary(event.getGuild().getIdLong(), options.range, options.source, userId);
|
||||
if(summary.isEmpty())
|
||||
{
|
||||
event.replyWarning("No playback analytics found for that report yet.");
|
||||
CommandLogContext.setError("no_data");
|
||||
return;
|
||||
}
|
||||
EmbedBuilder eb = userId == null
|
||||
? StatsMessageFormatter.summaryEmbed(title, summary, event.getSelfMember().getColorRaw())
|
||||
: StatsMessageFormatter.personalEmbed(title, summary, event.getSelfMember().getColorRaw());
|
||||
if(section == Section.SKIPS)
|
||||
{
|
||||
eb.clearFields();
|
||||
StatsMessageFormatter.addChunkedField(eb, "Skip actions", StatsMessageFormatter.countList(summary.getSkipActions(), "skips"), false);
|
||||
StatsMessageFormatter.addChunkedField(eb, "Skip votes", StatsMessageFormatter.countList(summary.getSkipVotes(), "votes"), false);
|
||||
}
|
||||
else if(section == Section.SOURCES)
|
||||
{
|
||||
eb.clearFields();
|
||||
StatsMessageFormatter.addChunkedField(eb, "Top sources", StatsMessageFormatter.countList(summary.getTopSources(), "plays"), false);
|
||||
}
|
||||
event.getChannel().sendMessageEmbeds(eb.build()).queue();
|
||||
CommandLogContext.setMeta(new JSONObject().put("range", options.range.getKey())
|
||||
.put("source", options.source == null ? JSONObject.NULL : options.source)
|
||||
.put("target_user_id", userId == null ? JSONObject.NULL : userId));
|
||||
}
|
||||
|
||||
private Options parseOptions(List<String> tokens)
|
||||
{
|
||||
StatsTimeRange range = StatsTimeRange.all();
|
||||
String source = null;
|
||||
int limit = 5;
|
||||
List<String> remaining = new ArrayList<>();
|
||||
List<String> userQueries = new ArrayList<>();
|
||||
for(String token : tokens)
|
||||
{
|
||||
if(token == null || token.isEmpty())
|
||||
continue;
|
||||
if(token.toLowerCase().startsWith("source:"))
|
||||
{
|
||||
source = StatsFilters.normalizeSource(token.substring("source:".length()));
|
||||
continue;
|
||||
}
|
||||
String lower = token.toLowerCase();
|
||||
if(lower.startsWith("limit:") || lower.startsWith("top:") || lower.startsWith("n:"))
|
||||
{
|
||||
Integer parsedLimit = parseLimit(token.substring(token.indexOf(':') + 1));
|
||||
if(parsedLimit != null)
|
||||
limit = parsedLimit;
|
||||
continue;
|
||||
}
|
||||
if(lower.startsWith("users:") || lower.startsWith("userlist:"))
|
||||
{
|
||||
String value = token.substring(token.indexOf(':') + 1).trim();
|
||||
if(value.startsWith("[") && value.endsWith("]"))
|
||||
value = value.substring(1, value.length() - 1);
|
||||
for(String part : value.split(","))
|
||||
if(!part.trim().isEmpty())
|
||||
userQueries.add(part.trim());
|
||||
continue;
|
||||
}
|
||||
StatsTimeRange parsedRange = StatsTimeRange.parse(token);
|
||||
if(parsedRange != null)
|
||||
{
|
||||
range = parsedRange;
|
||||
continue;
|
||||
}
|
||||
Integer parsedLimit = parseLimit(token);
|
||||
if(parsedLimit != null)
|
||||
{
|
||||
limit = parsedLimit;
|
||||
continue;
|
||||
}
|
||||
remaining.add(token);
|
||||
}
|
||||
return new Options(range, source, remaining, userQueries, limit);
|
||||
}
|
||||
|
||||
private Integer parseLimit(String token)
|
||||
{
|
||||
if(token == null || !token.matches("\\d+"))
|
||||
return null;
|
||||
int value = Integer.parseInt(token);
|
||||
if(value < 1)
|
||||
return 1;
|
||||
return Math.min(value, 25);
|
||||
}
|
||||
|
||||
private List<Long> resolveUserList(CommandEvent event, List<String> queries)
|
||||
{
|
||||
List<Long> userIds = new ArrayList<>();
|
||||
for(String query : queries)
|
||||
{
|
||||
Long userId = parseUserId(query);
|
||||
if(userId != null)
|
||||
{
|
||||
userIds.add(userId);
|
||||
continue;
|
||||
}
|
||||
List<Member> members = FinderUtil.findMembers(query, event.getGuild());
|
||||
if(members.isEmpty())
|
||||
members = findMembersByPartialName(query, event.getGuild().getMembers());
|
||||
if(members.isEmpty())
|
||||
{
|
||||
List<StatsRow> profileMatches = bot.getDataLogService().findUsersByProfile(event.getGuild().getIdLong(), query, 6);
|
||||
if(profileMatches.size() == 1)
|
||||
{
|
||||
userIds.add(profileMatches.get(0).getId());
|
||||
continue;
|
||||
}
|
||||
if(profileMatches.size() > 1)
|
||||
{
|
||||
event.replyWarning("Multiple analytics users matched `" + query + "`: " + formatProfileMatches(profileMatches) + ". Please be more specific or use a mention.");
|
||||
CommandLogContext.setError("member_ambiguous");
|
||||
return null;
|
||||
}
|
||||
event.replyWarning("No member found matching `" + query + "`. Try a mention or Discord user ID.");
|
||||
CommandLogContext.setError("member_not_found");
|
||||
return null;
|
||||
}
|
||||
if(members.size() > 1)
|
||||
{
|
||||
event.replyWarning("Multiple members matched `" + query + "`: " + formatMemberMatches(members) + ". Please be more specific or use a mention.");
|
||||
CommandLogContext.setError("member_ambiguous");
|
||||
return null;
|
||||
}
|
||||
userIds.add(members.get(0).getIdLong());
|
||||
}
|
||||
return userIds;
|
||||
}
|
||||
|
||||
private boolean canViewUserList(CommandEvent event, List<Long> userIds)
|
||||
{
|
||||
for(Long userId : userIds)
|
||||
if(userId != event.getAuthor().getIdLong())
|
||||
return canManageServer(event.getMember());
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean canManageServer(Member member)
|
||||
{
|
||||
return member != null && member.hasPermission(Permission.MANAGE_SERVER);
|
||||
}
|
||||
|
||||
private enum Section
|
||||
{
|
||||
ALL,
|
||||
SKIPS,
|
||||
SOURCES
|
||||
}
|
||||
|
||||
private static class Options
|
||||
{
|
||||
private final StatsTimeRange range;
|
||||
private final String source;
|
||||
private final List<String> remaining;
|
||||
private final List<String> userQueries;
|
||||
private final int limit;
|
||||
|
||||
private Options(StatsTimeRange range, String source, List<String> remaining, List<String> userQueries, int limit)
|
||||
{
|
||||
this.range = range == null ? StatsTimeRange.all() : range;
|
||||
this.source = source;
|
||||
this.remaining = remaining;
|
||||
this.userQueries = userQueries;
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.jagrosh.jmusicbot.commands.owner;
|
||||
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jdautilities.commons.JDAUtilitiesInfo;
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import com.jagrosh.jmusicbot.commands.OwnerCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
@@ -67,7 +66,7 @@ public class DebugCmd extends OwnerCommand
|
||||
.append("\n UpdateAlerts = ").append(bot.getConfig().useUpdateAlerts());
|
||||
sb.append("\n\nDependency Information:")
|
||||
.append("\n JDA Version = ").append(JDAInfo.VERSION)
|
||||
.append("\n JDA-Utilities Version = ").append(JDAUtilitiesInfo.VERSION)
|
||||
.append("\n JDA-Utilities Version = ").append(OtherUtil.getCommandLibraryVersion())
|
||||
.append("\n Lavaplayer Version = ").append(PlayerLibrary.VERSION);
|
||||
long total = Runtime.getRuntime().totalMemory() / 1024 / 1024;
|
||||
long used = total - (Runtime.getRuntime().freeMemory() / 1024 / 1024);
|
||||
|
||||
@@ -21,10 +21,15 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
@@ -259,6 +264,111 @@ public class DataLogService
|
||||
});
|
||||
}
|
||||
|
||||
public long getGuildTrackPlayCount(Guild guild, AudioTrack track)
|
||||
{
|
||||
return getGuildTrackPlayCount(guild, track, StatsTimeRange.all());
|
||||
}
|
||||
|
||||
public long getGuildTrackPlayCount(Guild guild, AudioTrack track, StatsTimeRange range)
|
||||
{
|
||||
if(guild == null || track == null)
|
||||
return 0L;
|
||||
AudioTrackInfo info = track.getInfo();
|
||||
return getGuildTrackPlayCount(guild.getIdLong(), inferSource(info), track.getIdentifier(), range);
|
||||
}
|
||||
|
||||
public long getGuildTrackPlayCount(long guildId, String source, String identifier)
|
||||
{
|
||||
return getGuildTrackPlayCount(guildId, source, identifier, StatsTimeRange.all());
|
||||
}
|
||||
|
||||
public long getGuildTrackPlayCount(long guildId, String source, String identifier, StatsTimeRange range)
|
||||
{
|
||||
if(identifier == null)
|
||||
return 0L;
|
||||
StatsTimeRange effectiveRange = range == null ? StatsTimeRange.all() : range;
|
||||
return query(() ->
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM play_sessions ps JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=? AND t.source=? AND t.identifier=?");
|
||||
if(effectiveRange.getStartMillis() != null)
|
||||
sql.append(" AND ps.started_at>=?");
|
||||
if(effectiveRange.getEndMillis() != null)
|
||||
sql.append(" AND ps.started_at<?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
ps.setLong(1, guildId);
|
||||
ps.setString(2, StatsFilters.normalizeSource(source));
|
||||
ps.setString(3, identifier);
|
||||
int index = 4;
|
||||
if(effectiveRange.getStartMillis() != null)
|
||||
ps.setLong(index++, effectiveRange.getStartMillis());
|
||||
if(effectiveRange.getEndMillis() != null)
|
||||
ps.setLong(index, effectiveRange.getEndMillis());
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
return rs.next() ? rs.getLong(1) : 0L;
|
||||
}
|
||||
}
|
||||
}, 0L);
|
||||
}
|
||||
|
||||
public StatsSummary getStatsSummary(long guildId, StatsTimeRange range, String source, Long userId)
|
||||
{
|
||||
StatsTimeRange effectiveRange = range == null ? StatsTimeRange.all() : range;
|
||||
String effectiveSource = StatsFilters.normalizeSource(source);
|
||||
return query(() -> buildStatsSummary(guildId, effectiveRange, effectiveSource, userId),
|
||||
new StatsSummary(effectiveRange, effectiveSource, 0L, 0L,
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList()));
|
||||
}
|
||||
|
||||
public List<StatsRow> getTopRequestedTracks(long guildId, StatsTimeRange range, String source, List<Long> userIds, int limit)
|
||||
{
|
||||
StatsTimeRange effectiveRange = range == null ? StatsTimeRange.all() : range;
|
||||
String effectiveSource = StatsFilters.normalizeSource(source);
|
||||
return query(() -> topRequestedTracks(guildId, effectiveRange, effectiveSource, userIds, Math.max(1, limit)),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
public List<StatsRow> getTopListenedTracks(long guildId, StatsTimeRange range, String source, List<Long> userIds, int limit)
|
||||
{
|
||||
StatsTimeRange effectiveRange = range == null ? StatsTimeRange.all() : range;
|
||||
String effectiveSource = StatsFilters.normalizeSource(source);
|
||||
return query(() -> topListenedTracks(guildId, effectiveRange, effectiveSource, userIds, Math.max(1, limit)),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
public List<StatsRow> findUsersByProfile(long guildId, String queryText, int limit)
|
||||
{
|
||||
if(queryText == null || queryText.trim().isEmpty())
|
||||
return Collections.emptyList();
|
||||
String like = "%" + queryText.trim().toLowerCase() + "%";
|
||||
return query(() ->
|
||||
{
|
||||
try(PreparedStatement ps = connection.prepareStatement(
|
||||
"SELECT up.user_id, COALESCE(up.nickname, up.username) label FROM user_profiles up "
|
||||
+ "WHERE up.guild_id=? AND up.seen_at=(SELECT MAX(seen_at) FROM user_profiles latest WHERE latest.guild_id=up.guild_id AND latest.user_id=up.user_id) "
|
||||
+ "AND (LOWER(COALESCE(up.username, '')) LIKE ? OR LOWER(COALESCE(up.nickname, '')) LIKE ?) "
|
||||
+ "ORDER BY label ASC LIMIT ?"))
|
||||
{
|
||||
ps.setLong(1, guildId);
|
||||
ps.setString(2, like);
|
||||
ps.setString(3, like);
|
||||
ps.setInt(4, Math.max(1, limit));
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
{
|
||||
long userId = rs.getLong("user_id");
|
||||
rows.add(new StatsRow(userId, userLabel(rs.getString("label"), userId), null, 0L, 0L));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}, Collections.emptyList());
|
||||
}
|
||||
|
||||
public void logQueueEventWithMeta(Guild guild, User user, AudioTrack track, String eventType, String source, Integer position, String query, String playlistName, String metaJson)
|
||||
{
|
||||
if(guild == null || eventType == null)
|
||||
@@ -612,6 +722,360 @@ public class DataLogService
|
||||
executor.submit(r);
|
||||
}
|
||||
|
||||
private <T> T query(SqlSupplier<T> supplier, T fallback)
|
||||
{
|
||||
Future<T> future = executor.submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return supplier.get();
|
||||
}
|
||||
catch(SQLException ex)
|
||||
{
|
||||
LOG.debug("stats query failed: {}", ex.getMessage());
|
||||
return fallback;
|
||||
}
|
||||
});
|
||||
try
|
||||
{
|
||||
return future.get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
future.cancel(true);
|
||||
LOG.debug("stats query timed out or failed: {}", ex.getMessage());
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private StatsSummary buildStatsSummary(long guildId, StatsTimeRange range, String source, Long userId) throws SQLException
|
||||
{
|
||||
long totalPlays = countPlays(guildId, range, source, userId);
|
||||
long totalListeningMillis = sumListeningMillis(guildId, range, source, userId);
|
||||
return new StatsSummary(range, source, totalPlays, totalListeningMillis,
|
||||
topTracks(guildId, range, source, userId, 10),
|
||||
topUsersFromPlaySessions(guildId, range, source, userId, 10),
|
||||
topListeners(guildId, range, source, userId, 10),
|
||||
topSources(guildId, range, userId, 10),
|
||||
topSkippers(guildId, range, source, userId, true, 10),
|
||||
topSkippers(guildId, range, source, userId, false, 10));
|
||||
}
|
||||
|
||||
private long countPlays(long guildId, StatsTimeRange range, String source, Long userId) throws SQLException
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM play_sessions ps JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, source, userId);
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
bindPlayFilters(ps, 1, guildId, range, source, userId);
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
return rs.next() ? rs.getLong(1) : 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long sumListeningMillis(long guildId, StatsTimeRange range, String source, Long userId) throws SQLException
|
||||
{
|
||||
String upperBound = boundedListenerUpperExpression();
|
||||
StringBuilder sql = new StringBuilder("SELECT SUM(CASE "
|
||||
+ "WHEN " + upperBound + " > MAX(ls.joined_at, COALESCE(ps.started_at, ls.joined_at)) "
|
||||
+ "THEN " + upperBound + " - MAX(ls.joined_at, COALESCE(ps.started_at, ls.joined_at)) "
|
||||
+ "ELSE 0 END) "
|
||||
+ "FROM listener_sessions ls JOIN play_sessions ps ON ls.play_session_id=ps.id JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, source, null);
|
||||
if(userId != null)
|
||||
sql.append(" AND ls.user_id=?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = 1;
|
||||
long now = now();
|
||||
ps.setLong(index++, now);
|
||||
ps.setLong(index++, now);
|
||||
ps.setLong(index++, now);
|
||||
ps.setLong(index++, now);
|
||||
index = bindPlayFilters(ps, index, guildId, range, source, null);
|
||||
if(userId != null)
|
||||
ps.setLong(index, userId);
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
return rs.next() ? rs.getLong(1) : 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topTracks(long guildId, StatsTimeRange range, String source, Long userId, int limit) throws SQLException
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT t.id, t.title, t.author, COUNT(*) plays FROM play_sessions ps JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, source, userId);
|
||||
sql.append(" GROUP BY t.id, t.title, t.author ORDER BY plays DESC, t.title ASC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = bindPlayFilters(ps, 1, guildId, range, source, userId);
|
||||
ps.setInt(index, limit);
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
rows.add(new StatsRow(rs.getLong("id"), safeLabel(rs.getString("title"), "Unknown track"), rs.getString("author"), rs.getLong("plays"), 0L));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topRequestedTracks(long guildId, StatsTimeRange range, String source, List<Long> userIds, int limit) throws SQLException
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT t.id, t.title, t.author, COUNT(*) plays FROM play_sessions ps JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, source, null);
|
||||
appendUserListFilter(sql, "ps.requested_by", userIds);
|
||||
sql.append(" GROUP BY t.id, t.title, t.author ORDER BY plays DESC, t.title ASC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = bindPlayFilters(ps, 1, guildId, range, source, null);
|
||||
index = bindUserList(ps, index, userIds);
|
||||
ps.setInt(index, limit);
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
rows.add(new StatsRow(rs.getLong("id"), safeLabel(rs.getString("title"), "Unknown track"), rs.getString("author"), rs.getLong("plays"), 0L));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topListenedTracks(long guildId, StatsTimeRange range, String source, List<Long> userIds, int limit) throws SQLException
|
||||
{
|
||||
String upperBound = boundedListenerUpperExpression();
|
||||
StringBuilder sql = new StringBuilder("SELECT t.id, t.title, t.author, COUNT(*) listens, "
|
||||
+ "SUM(CASE "
|
||||
+ "WHEN " + upperBound + " > MAX(ls.joined_at, COALESCE(ps.started_at, ls.joined_at)) "
|
||||
+ "THEN " + upperBound + " - MAX(ls.joined_at, COALESCE(ps.started_at, ls.joined_at)) "
|
||||
+ "ELSE 0 END) listen_ms "
|
||||
+ "FROM listener_sessions ls JOIN play_sessions ps ON ls.play_session_id=ps.id JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, source, null);
|
||||
appendUserListFilter(sql, "ls.user_id", userIds);
|
||||
sql.append(" GROUP BY t.id, t.title, t.author ORDER BY listen_ms DESC, listens DESC, t.title ASC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = 1;
|
||||
long current = now();
|
||||
ps.setLong(index++, current);
|
||||
ps.setLong(index++, current);
|
||||
ps.setLong(index++, current);
|
||||
ps.setLong(index++, current);
|
||||
index = bindPlayFilters(ps, index, guildId, range, source, null);
|
||||
index = bindUserList(ps, index, userIds);
|
||||
ps.setInt(index, limit);
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
rows.add(new StatsRow(rs.getLong("id"), safeLabel(rs.getString("title"), "Unknown track"), rs.getString("author"), rs.getLong("listens"), rs.getLong("listen_ms")));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topUsersFromPlaySessions(long guildId, StatsTimeRange range, String source, Long userId, int limit) throws SQLException
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT ps.requested_by user_id, " + userLabelSql("ps.requested_by", "ps.guild_id") + " label, COUNT(*) plays "
|
||||
+ "FROM play_sessions ps JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=? AND ps.requested_by<>0");
|
||||
appendPlayFilters(sql, range, source, userId);
|
||||
sql.append(" GROUP BY ps.requested_by ORDER BY plays DESC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = bindPlayFilters(ps, 1, guildId, range, source, userId);
|
||||
ps.setInt(index, limit);
|
||||
return readCountRows(ps, "user_id", "label", "plays");
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topListeners(long guildId, StatsTimeRange range, String source, Long userId, int limit) throws SQLException
|
||||
{
|
||||
String upperBound = boundedListenerUpperExpression();
|
||||
StringBuilder sql = new StringBuilder("SELECT ls.user_id, " + userLabelSql("ls.user_id", "ps.guild_id") + " label, "
|
||||
+ "SUM(CASE "
|
||||
+ "WHEN " + upperBound + " > MAX(ls.joined_at, COALESCE(ps.started_at, ls.joined_at)) "
|
||||
+ "THEN " + upperBound + " - MAX(ls.joined_at, COALESCE(ps.started_at, ls.joined_at)) "
|
||||
+ "ELSE 0 END) listen_ms "
|
||||
+ "FROM listener_sessions ls JOIN play_sessions ps ON ls.play_session_id=ps.id JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, source, null);
|
||||
if(userId != null)
|
||||
sql.append(" AND ls.user_id=?");
|
||||
sql.append(" GROUP BY ls.user_id ORDER BY listen_ms DESC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = 1;
|
||||
long current = now();
|
||||
ps.setLong(index++, current);
|
||||
ps.setLong(index++, current);
|
||||
ps.setLong(index++, current);
|
||||
ps.setLong(index++, current);
|
||||
index = bindPlayFilters(ps, index, guildId, range, source, null);
|
||||
if(userId != null)
|
||||
ps.setLong(index++, userId);
|
||||
ps.setInt(index, limit);
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
rows.add(new StatsRow(rs.getLong("user_id"), userLabel(rs.getString("label"), rs.getLong("user_id")), null, 0L, rs.getLong("listen_ms")));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topSources(long guildId, StatsTimeRange range, Long userId, int limit) throws SQLException
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT t.source, COUNT(*) plays FROM play_sessions ps JOIN tracks t ON ps.track_id=t.id WHERE ps.guild_id=?");
|
||||
appendPlayFilters(sql, range, null, userId);
|
||||
sql.append(" GROUP BY t.source ORDER BY plays DESC, t.source ASC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = bindPlayFilters(ps, 1, guildId, range, null, userId);
|
||||
ps.setInt(index, limit);
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
rows.add(new StatsRow(0L, safeLabel(rs.getString("source"), "unknown"), null, rs.getLong("plays"), 0L));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> topSkippers(long guildId, StatsTimeRange range, String source, Long userId, boolean votes, int limit) throws SQLException
|
||||
{
|
||||
StringBuilder sql = new StringBuilder("SELECT qe.user_id, " + userLabelSql("qe.user_id", "qe.guild_id") + " label, COUNT(*) skips "
|
||||
+ "FROM queue_events qe LEFT JOIN tracks t ON qe.track_id=t.id WHERE qe.guild_id=? AND qe.user_id<>0");
|
||||
if(votes)
|
||||
sql.append(" AND qe.event_type='SKIP_VOTE'");
|
||||
else
|
||||
sql.append(" AND qe.event_type LIKE 'SKIP_%' AND qe.event_type<>'SKIP_VOTE'");
|
||||
appendQueueFilters(sql, range, source, userId);
|
||||
sql.append(" GROUP BY qe.user_id ORDER BY skips DESC LIMIT ?");
|
||||
try(PreparedStatement ps = connection.prepareStatement(sql.toString()))
|
||||
{
|
||||
int index = bindQueueFilters(ps, 1, guildId, range, source, userId);
|
||||
ps.setInt(index, limit);
|
||||
return readCountRows(ps, "user_id", "label", "skips");
|
||||
}
|
||||
}
|
||||
|
||||
private List<StatsRow> readCountRows(PreparedStatement ps, String idColumn, String labelColumn, String countColumn) throws SQLException
|
||||
{
|
||||
List<StatsRow> rows = new ArrayList<>();
|
||||
try(ResultSet rs = ps.executeQuery())
|
||||
{
|
||||
while(rs.next())
|
||||
{
|
||||
long id = rs.getLong(idColumn);
|
||||
rows.add(new StatsRow(id, userLabel(rs.getString(labelColumn), id), null, rs.getLong(countColumn), 0L));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void appendPlayFilters(StringBuilder sql, StatsTimeRange range, String source, Long userId)
|
||||
{
|
||||
if(range != null && range.getStartMillis() != null)
|
||||
sql.append(" AND ps.started_at>=?");
|
||||
if(range != null && range.getEndMillis() != null)
|
||||
sql.append(" AND ps.started_at<?");
|
||||
if(source != null)
|
||||
sql.append(" AND t.source=?");
|
||||
if(userId != null)
|
||||
sql.append(" AND ps.requested_by=?");
|
||||
}
|
||||
|
||||
private int bindPlayFilters(PreparedStatement ps, int index, long guildId, StatsTimeRange range, String source, Long userId) throws SQLException
|
||||
{
|
||||
ps.setLong(index++, guildId);
|
||||
if(range != null && range.getStartMillis() != null)
|
||||
ps.setLong(index++, range.getStartMillis());
|
||||
if(range != null && range.getEndMillis() != null)
|
||||
ps.setLong(index++, range.getEndMillis());
|
||||
if(source != null)
|
||||
ps.setString(index++, source);
|
||||
if(userId != null)
|
||||
ps.setLong(index++, userId);
|
||||
return index;
|
||||
}
|
||||
|
||||
private void appendQueueFilters(StringBuilder sql, StatsTimeRange range, String source, Long userId)
|
||||
{
|
||||
if(range != null && range.getStartMillis() != null)
|
||||
sql.append(" AND qe.created_at>=?");
|
||||
if(range != null && range.getEndMillis() != null)
|
||||
sql.append(" AND qe.created_at<?");
|
||||
if(source != null)
|
||||
sql.append(" AND t.source=?");
|
||||
if(userId != null)
|
||||
sql.append(" AND qe.user_id=?");
|
||||
}
|
||||
|
||||
private int bindQueueFilters(PreparedStatement ps, int index, long guildId, StatsTimeRange range, String source, Long userId) throws SQLException
|
||||
{
|
||||
ps.setLong(index++, guildId);
|
||||
if(range != null && range.getStartMillis() != null)
|
||||
ps.setLong(index++, range.getStartMillis());
|
||||
if(range != null && range.getEndMillis() != null)
|
||||
ps.setLong(index++, range.getEndMillis());
|
||||
if(source != null)
|
||||
ps.setString(index++, source);
|
||||
if(userId != null)
|
||||
ps.setLong(index++, userId);
|
||||
return index;
|
||||
}
|
||||
|
||||
private void appendUserListFilter(StringBuilder sql, String column, List<Long> userIds)
|
||||
{
|
||||
if(userIds == null || userIds.isEmpty())
|
||||
return;
|
||||
sql.append(" AND ").append(column).append(" IN (");
|
||||
for(int i = 0; i < userIds.size(); i++)
|
||||
{
|
||||
if(i > 0)
|
||||
sql.append(",");
|
||||
sql.append("?");
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
|
||||
private int bindUserList(PreparedStatement ps, int index, List<Long> userIds) throws SQLException
|
||||
{
|
||||
if(userIds == null)
|
||||
return index;
|
||||
for(Long userId : userIds)
|
||||
ps.setLong(index++, userId);
|
||||
return index;
|
||||
}
|
||||
|
||||
private String userLabelSql(String userIdExpression, String guildIdExpression)
|
||||
{
|
||||
return "(SELECT COALESCE(nickname, username) FROM user_profiles up WHERE up.user_id=" + userIdExpression + " AND up.guild_id=" + guildIdExpression + " ORDER BY seen_at DESC LIMIT 1)";
|
||||
}
|
||||
|
||||
private String boundedListenerUpperExpression()
|
||||
{
|
||||
String fallbackEnd = "COALESCE(ps.ended_at, CASE "
|
||||
+ "WHEN ps.duration_ms IS NOT NULL THEN COALESCE(ps.started_at, ls.joined_at) + ps.duration_ms "
|
||||
+ "WHEN t.is_stream=0 AND t.length_ms IS NOT NULL THEN COALESCE(ps.started_at, ls.joined_at) + t.length_ms "
|
||||
+ "ELSE ? END)";
|
||||
return "MIN(COALESCE(ls.left_at, " + fallbackEnd + "), " + fallbackEnd + ")";
|
||||
}
|
||||
|
||||
private String userLabel(String label, long userId)
|
||||
{
|
||||
return safeLabel(label, "<@" + userId + ">");
|
||||
}
|
||||
|
||||
private String safeLabel(String label, String fallback)
|
||||
{
|
||||
return label == null || label.trim().isEmpty() ? fallback : label;
|
||||
}
|
||||
|
||||
private static long now()
|
||||
{
|
||||
return Instant.now().toEpochMilli();
|
||||
@@ -626,4 +1090,9 @@ public class DataLogService
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
private interface SqlSupplier<T>
|
||||
{
|
||||
T get() throws SQLException;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
public final class StatsAccess
|
||||
{
|
||||
private StatsAccess() {}
|
||||
|
||||
public static boolean canViewUserStats(long requesterId, long targetId, boolean canManageServer)
|
||||
{
|
||||
return requesterId == targetId || canManageServer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
public final class StatsFilters
|
||||
{
|
||||
private StatsFilters() {}
|
||||
|
||||
public static String normalizeSource(String source)
|
||||
{
|
||||
if(source == null)
|
||||
return null;
|
||||
String normalized = source.trim().toLowerCase();
|
||||
if(normalized.isEmpty() || "all".equals(normalized) || "any".equals(normalized))
|
||||
return null;
|
||||
if("yt".equals(normalized) || "youtube.com".equals(normalized) || "youtu.be".equals(normalized))
|
||||
return "youtube";
|
||||
if("sc".equals(normalized) || "soundcloud.com".equals(normalized))
|
||||
return "soundcloud";
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
import com.jagrosh.jmusicbot.utils.TimeUtil;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
|
||||
public final class StatsMessageFormatter
|
||||
{
|
||||
private static final int EMBED_FIELD_VALUE_LIMIT = 1024;
|
||||
private static final int SAFE_FIELD_VALUE_LIMIT = 1000;
|
||||
|
||||
private StatsMessageFormatter() {}
|
||||
|
||||
public static EmbedBuilder summaryEmbed(String title, StatsSummary summary, int color)
|
||||
{
|
||||
EmbedBuilder eb = new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setColor(color)
|
||||
.setDescription("Range: **" + summary.getRange().getLabel() + "**"
|
||||
+ (summary.getSource() == null ? "" : "\nSource: **" + summary.getSource() + "**")
|
||||
+ "\nPlays: **" + summary.getTotalPlays() + "**"
|
||||
+ "\nListening time: **" + TimeUtil.formatTime(summary.getTotalListeningMillis()) + "**");
|
||||
addCountField(eb, "Top tracks", summary.getTopTracks(), "plays");
|
||||
addTimeField(eb, "Top listeners", summary.getTopListeners());
|
||||
addCountField(eb, "Top requesters", summary.getTopRequesters(), "plays");
|
||||
addCountField(eb, "Top sources", summary.getTopSources(), "plays");
|
||||
addCountField(eb, "Skip actions", summary.getSkipActions(), "skips");
|
||||
addCountField(eb, "Skip votes", summary.getSkipVotes(), "votes");
|
||||
return eb;
|
||||
}
|
||||
|
||||
public static EmbedBuilder personalEmbed(String title, StatsSummary summary, int color)
|
||||
{
|
||||
long skipActions = sumCounts(summary.getSkipActions());
|
||||
long skipVotes = sumCounts(summary.getSkipVotes());
|
||||
EmbedBuilder eb = new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setColor(color)
|
||||
.setDescription("Range: **" + summary.getRange().getLabel() + "**"
|
||||
+ (summary.getSource() == null ? "" : "\nSource: **" + summary.getSource() + "**")
|
||||
+ "\nRequests: **" + summary.getTotalPlays() + "**"
|
||||
+ "\nListening time: **" + TimeUtil.formatTime(summary.getTotalListeningMillis()) + "**"
|
||||
+ "\nSkip actions: **" + skipActions + "**"
|
||||
+ "\nSkip votes: **" + skipVotes + "**");
|
||||
addCountField(eb, "Top tracks", summary.getTopTracks(), "plays");
|
||||
addCountField(eb, "Top sources", summary.getTopSources(), "plays");
|
||||
return eb;
|
||||
}
|
||||
|
||||
public static String countList(Iterable<StatsRow> rows, String unit)
|
||||
{
|
||||
return countList(rows, unit, 5);
|
||||
}
|
||||
|
||||
public static String countList(Iterable<StatsRow> rows, String unit, int limit)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int index = 1;
|
||||
for(StatsRow row : rows)
|
||||
{
|
||||
if(index > limit)
|
||||
break;
|
||||
builder.append(index++).append(". **").append(row.getLabel()).append("** - ")
|
||||
.append(row.getCount()).append(" ").append(unit);
|
||||
if(row.getDetail() != null && !row.getDetail().isEmpty())
|
||||
builder.append(" (").append(row.getDetail()).append(")");
|
||||
builder.append("\n");
|
||||
}
|
||||
return builder.length() == 0 ? "No data" : builder.toString();
|
||||
}
|
||||
|
||||
public static String timeList(Iterable<StatsRow> rows)
|
||||
{
|
||||
return timeList(rows, 5);
|
||||
}
|
||||
|
||||
public static String timeList(Iterable<StatsRow> rows, int limit)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int index = 1;
|
||||
for(StatsRow row : rows)
|
||||
{
|
||||
if(index > limit)
|
||||
break;
|
||||
builder.append(index++).append(". **").append(row.getLabel()).append("** - ")
|
||||
.append(TimeUtil.formatTime(row.getMillis())).append("\n");
|
||||
}
|
||||
return builder.length() == 0 ? "No data" : builder.toString();
|
||||
}
|
||||
|
||||
public static String trackListenList(Iterable<StatsRow> rows, int limit)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int index = 1;
|
||||
for(StatsRow row : rows)
|
||||
{
|
||||
if(index > limit)
|
||||
break;
|
||||
builder.append(index++).append(". **").append(row.getLabel()).append("** - ")
|
||||
.append(TimeUtil.formatTime(row.getMillis()));
|
||||
if(row.getCount() > 0L)
|
||||
builder.append(" (").append(row.getCount()).append(" listen").append(row.getCount() == 1L ? "" : "s").append(")");
|
||||
if(row.getDetail() != null && !row.getDetail().isEmpty())
|
||||
builder.append(" (").append(row.getDetail()).append(")");
|
||||
builder.append("\n");
|
||||
}
|
||||
return builder.length() == 0 ? "No data" : builder.toString();
|
||||
}
|
||||
|
||||
private static void addCountField(EmbedBuilder eb, String name, Iterable<StatsRow> rows, String unit)
|
||||
{
|
||||
addChunkedField(eb, name, countList(rows, unit), false);
|
||||
}
|
||||
|
||||
private static void addTimeField(EmbedBuilder eb, String name, Iterable<StatsRow> rows)
|
||||
{
|
||||
addChunkedField(eb, name, timeList(rows), false);
|
||||
}
|
||||
|
||||
public static void addChunkedField(EmbedBuilder eb, String name, String value, boolean inline)
|
||||
{
|
||||
if(value == null || value.isEmpty())
|
||||
{
|
||||
eb.addField(name, "No data", inline);
|
||||
return;
|
||||
}
|
||||
|
||||
String[] lines = value.split("\\n");
|
||||
StringBuilder chunk = new StringBuilder();
|
||||
int part = 1;
|
||||
for(String rawLine : lines)
|
||||
{
|
||||
if(rawLine == null || rawLine.isEmpty())
|
||||
continue;
|
||||
String line = fitFieldLine(rawLine);
|
||||
int nextLength = chunk.length() + line.length() + 1;
|
||||
if(chunk.length() > 0 && nextLength > SAFE_FIELD_VALUE_LIMIT)
|
||||
{
|
||||
eb.addField(partName(name, part++), chunk.toString(), inline);
|
||||
chunk.setLength(0);
|
||||
}
|
||||
if(chunk.length() > 0)
|
||||
chunk.append("\n");
|
||||
chunk.append(line);
|
||||
}
|
||||
if(chunk.length() == 0)
|
||||
chunk.append("No data");
|
||||
eb.addField(partName(name, part), chunk.toString(), inline);
|
||||
}
|
||||
|
||||
private static String partName(String name, int part)
|
||||
{
|
||||
return part <= 1 ? name : name + " " + part;
|
||||
}
|
||||
|
||||
private static String fitFieldLine(String line)
|
||||
{
|
||||
if(line.length() <= EMBED_FIELD_VALUE_LIMIT)
|
||||
return line;
|
||||
return line.substring(0, SAFE_FIELD_VALUE_LIMIT - 3) + "...";
|
||||
}
|
||||
|
||||
private static long sumCounts(Iterable<StatsRow> rows)
|
||||
{
|
||||
long count = 0L;
|
||||
for(StatsRow row : rows)
|
||||
count += row.getCount();
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import com.jagrosh.jmusicbot.settings.Settings;
|
||||
import com.jagrosh.jmusicbot.settings.StatsReportFrequency;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class StatsReportScheduler
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger("StatsReports");
|
||||
|
||||
private final Bot bot;
|
||||
private final Map<Long, String> sentPeriods = new ConcurrentHashMap<>();
|
||||
|
||||
public StatsReportScheduler(Bot bot)
|
||||
{
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
public void init()
|
||||
{
|
||||
bot.getThreadpool().scheduleWithFixedDelay(() -> runScheduledReports(), 1, 1, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
private void runScheduledReports()
|
||||
{
|
||||
if(bot.getDataLogService() == null)
|
||||
return;
|
||||
JDA jda = bot.getJDA();
|
||||
if(jda == null)
|
||||
return;
|
||||
LocalDate today = LocalDate.now();
|
||||
for(Guild guild : jda.getGuilds())
|
||||
{
|
||||
Settings settings = bot.getSettingsManager().getSettings(guild);
|
||||
if(settings.getStatsReportFrequency() == StatsReportFrequency.OFF)
|
||||
continue;
|
||||
if(settings.getStatsReportFrequency() == StatsReportFrequency.MONTHLY && today.getDayOfMonth() != 1)
|
||||
continue;
|
||||
if(settings.getStatsReportFrequency() == StatsReportFrequency.YEARLY && (today.getDayOfMonth() != 1 || today.getMonthValue() != 1))
|
||||
continue;
|
||||
|
||||
TextChannel channel = settings.getStatsReportChannel(guild);
|
||||
if(channel == null)
|
||||
continue;
|
||||
StatsTimeRange range = settings.getStatsReportFrequency() == StatsReportFrequency.YEARLY
|
||||
? StatsTimeRange.previousYear()
|
||||
: StatsTimeRange.previousMonth();
|
||||
String periodKey = settings.getStatsReportFrequency().name() + ":" + range.getLabel();
|
||||
if(periodKey.equals(sentPeriods.get(guild.getIdLong())))
|
||||
continue;
|
||||
if(sendReport(guild, channel, range))
|
||||
sentPeriods.put(guild.getIdLong(), periodKey);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendReport(Guild guild, TextChannel channel, StatsTimeRange range)
|
||||
{
|
||||
if(bot.getDataLogService() == null || guild == null || channel == null)
|
||||
return false;
|
||||
StatsSummary summary = bot.getDataLogService().getStatsSummary(guild.getIdLong(), range, null, null);
|
||||
if(summary.isEmpty())
|
||||
return false;
|
||||
channel.sendMessageEmbeds(StatsMessageFormatter.summaryEmbed("Server rewind - " + range.getLabel(), summary,
|
||||
guild.getSelfMember().getColorRaw()).build()).queue(
|
||||
m -> {},
|
||||
t -> LOG.debug("Failed to send stats report for guild {}: {}", guild.getId(), t.getMessage()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
public class StatsRow
|
||||
{
|
||||
private final long id;
|
||||
private final String label;
|
||||
private final String detail;
|
||||
private final long count;
|
||||
private final long millis;
|
||||
|
||||
public StatsRow(long id, String label, String detail, long count, long millis)
|
||||
{
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.detail = detail;
|
||||
this.count = count;
|
||||
this.millis = millis;
|
||||
}
|
||||
|
||||
public long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
public String getDetail()
|
||||
{
|
||||
return detail;
|
||||
}
|
||||
|
||||
public long getCount()
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
public long getMillis()
|
||||
{
|
||||
return millis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class StatsSummary
|
||||
{
|
||||
private final StatsTimeRange range;
|
||||
private final String source;
|
||||
private final long totalPlays;
|
||||
private final long totalListeningMillis;
|
||||
private final List<StatsRow> topTracks;
|
||||
private final List<StatsRow> topRequesters;
|
||||
private final List<StatsRow> topListeners;
|
||||
private final List<StatsRow> topSources;
|
||||
private final List<StatsRow> skipVotes;
|
||||
private final List<StatsRow> skipActions;
|
||||
|
||||
public StatsSummary(StatsTimeRange range, String source, long totalPlays, long totalListeningMillis,
|
||||
List<StatsRow> topTracks, List<StatsRow> topRequesters, List<StatsRow> topListeners,
|
||||
List<StatsRow> topSources, List<StatsRow> skipVotes, List<StatsRow> skipActions)
|
||||
{
|
||||
this.range = range;
|
||||
this.source = source;
|
||||
this.totalPlays = totalPlays;
|
||||
this.totalListeningMillis = totalListeningMillis;
|
||||
this.topTracks = topTracks == null ? Collections.emptyList() : topTracks;
|
||||
this.topRequesters = topRequesters == null ? Collections.emptyList() : topRequesters;
|
||||
this.topListeners = topListeners == null ? Collections.emptyList() : topListeners;
|
||||
this.topSources = topSources == null ? Collections.emptyList() : topSources;
|
||||
this.skipVotes = skipVotes == null ? Collections.emptyList() : skipVotes;
|
||||
this.skipActions = skipActions == null ? Collections.emptyList() : skipActions;
|
||||
}
|
||||
|
||||
public StatsTimeRange getRange()
|
||||
{
|
||||
return range;
|
||||
}
|
||||
|
||||
public String getSource()
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
public long getTotalPlays()
|
||||
{
|
||||
return totalPlays;
|
||||
}
|
||||
|
||||
public long getTotalListeningMillis()
|
||||
{
|
||||
return totalListeningMillis;
|
||||
}
|
||||
|
||||
public List<StatsRow> getTopTracks()
|
||||
{
|
||||
return topTracks;
|
||||
}
|
||||
|
||||
public List<StatsRow> getTopRequesters()
|
||||
{
|
||||
return topRequesters;
|
||||
}
|
||||
|
||||
public List<StatsRow> getTopListeners()
|
||||
{
|
||||
return topListeners;
|
||||
}
|
||||
|
||||
public List<StatsRow> getTopSources()
|
||||
{
|
||||
return topSources;
|
||||
}
|
||||
|
||||
public List<StatsRow> getSkipVotes()
|
||||
{
|
||||
return skipVotes;
|
||||
}
|
||||
|
||||
public List<StatsRow> getSkipActions()
|
||||
{
|
||||
return skipActions;
|
||||
}
|
||||
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return totalPlays == 0L && totalListeningMillis == 0L
|
||||
&& topTracks.isEmpty() && topRequesters.isEmpty() && topListeners.isEmpty()
|
||||
&& topSources.isEmpty() && skipVotes.isEmpty() && skipActions.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.datalog;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
public class StatsTimeRange
|
||||
{
|
||||
private final String key;
|
||||
private final String label;
|
||||
private final Long startMillis;
|
||||
private final Long endMillis;
|
||||
|
||||
private StatsTimeRange(String key, String label, Long startMillis, Long endMillis)
|
||||
{
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.startMillis = startMillis;
|
||||
this.endMillis = endMillis;
|
||||
}
|
||||
|
||||
public static StatsTimeRange all()
|
||||
{
|
||||
return new StatsTimeRange("all", "all time", null, null);
|
||||
}
|
||||
|
||||
public static StatsTimeRange parse(String token)
|
||||
{
|
||||
if(token == null || token.trim().isEmpty())
|
||||
return all();
|
||||
String normalized = token.trim().toLowerCase();
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ZonedDateTime start;
|
||||
if(normalized.matches("\\d+[dwmy]"))
|
||||
{
|
||||
int amount = Integer.parseInt(normalized.substring(0, normalized.length() - 1));
|
||||
char unit = normalized.charAt(normalized.length() - 1);
|
||||
switch(unit)
|
||||
{
|
||||
case 'd':
|
||||
start = now.minusDays(amount);
|
||||
return new StatsTimeRange(normalized, "last " + amount + " day" + (amount == 1 ? "" : "s"), start.toInstant().toEpochMilli(), null);
|
||||
case 'w':
|
||||
start = now.minusWeeks(amount);
|
||||
return new StatsTimeRange(normalized, "last " + amount + " week" + (amount == 1 ? "" : "s"), start.toInstant().toEpochMilli(), null);
|
||||
case 'm':
|
||||
start = now.minusMonths(amount);
|
||||
return new StatsTimeRange(normalized, "last " + amount + " month" + (amount == 1 ? "" : "s"), start.toInstant().toEpochMilli(), null);
|
||||
case 'y':
|
||||
start = now.minusYears(amount);
|
||||
return new StatsTimeRange(normalized, "last " + amount + " year" + (amount == 1 ? "" : "s"), start.toInstant().toEpochMilli(), null);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch(normalized)
|
||||
{
|
||||
case "today":
|
||||
case "day":
|
||||
start = now.toLocalDate().atStartOfDay(now.getZone());
|
||||
return new StatsTimeRange("today", "today", start.toInstant().toEpochMilli(), null);
|
||||
case "week":
|
||||
case "thisweek":
|
||||
LocalDate monday = now.toLocalDate().with(DayOfWeek.MONDAY);
|
||||
start = monday.atStartOfDay(now.getZone());
|
||||
return new StatsTimeRange("week", "this week", start.toInstant().toEpochMilli(), null);
|
||||
case "month":
|
||||
case "thismonth":
|
||||
start = now.withDayOfMonth(1).toLocalDate().atStartOfDay(now.getZone());
|
||||
return new StatsTimeRange("month", "this month", start.toInstant().toEpochMilli(), null);
|
||||
case "year":
|
||||
case "thisyear":
|
||||
start = now.withDayOfYear(1).toLocalDate().atStartOfDay(now.getZone());
|
||||
return new StatsTimeRange("year", "this year", start.toInstant().toEpochMilli(), null);
|
||||
case "all":
|
||||
case "alltime":
|
||||
return all();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static StatsTimeRange previousMonth()
|
||||
{
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ZonedDateTime start = now.minusMonths(1).withDayOfMonth(1).toLocalDate().atStartOfDay(now.getZone());
|
||||
ZonedDateTime end = start.plusMonths(1);
|
||||
return new StatsTimeRange("month", start.getMonth() + " " + start.getYear(), start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
|
||||
}
|
||||
|
||||
public static StatsTimeRange month(int year, int month)
|
||||
{
|
||||
if(month < 1 || month > 12)
|
||||
return null;
|
||||
ZonedDateTime start = ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
ZonedDateTime end = start.plusMonths(1);
|
||||
return new StatsTimeRange(String.format("%04d-%02d", year, month), Month.of(month) + " " + year,
|
||||
start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
|
||||
}
|
||||
|
||||
public static StatsTimeRange previousYear()
|
||||
{
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ZonedDateTime start = ZonedDateTime.of(now.getYear() - 1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
ZonedDateTime end = start.plusYears(1);
|
||||
return new StatsTimeRange("year", Integer.toString(start.getYear()), start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
|
||||
}
|
||||
|
||||
public static StatsTimeRange year(int year)
|
||||
{
|
||||
ZonedDateTime start = ZonedDateTime.of(year, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
ZonedDateTime end = start.plusYears(1);
|
||||
return new StatsTimeRange(Integer.toString(year), Integer.toString(year), start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
|
||||
}
|
||||
|
||||
public String getKey()
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
public Long getStartMillis()
|
||||
{
|
||||
return startMillis;
|
||||
}
|
||||
|
||||
public Long getEndMillis()
|
||||
{
|
||||
return endMillis;
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,15 @@ public class Settings implements GuildSettingsProvider
|
||||
private QueueType queueType;
|
||||
private String prefix;
|
||||
private double skipRatio;
|
||||
private long statsReportChannelId;
|
||||
private StatsReportFrequency statsReportFrequency;
|
||||
|
||||
public Settings(SettingsManager manager, String textId, String voiceId, String roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType)
|
||||
{
|
||||
this(manager, textId, voiceId, roleId, volume, defaultPlaylist, repeatMode, prefix, skipRatio, queueType, null, StatsReportFrequency.OFF);
|
||||
}
|
||||
|
||||
public Settings(SettingsManager manager, String textId, String voiceId, String roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType, String statsReportChannelId, StatsReportFrequency statsReportFrequency)
|
||||
{
|
||||
this.manager = manager;
|
||||
try
|
||||
@@ -73,9 +80,23 @@ public class Settings implements GuildSettingsProvider
|
||||
this.prefix = prefix;
|
||||
this.skipRatio = skipRatio;
|
||||
this.queueType = queueType;
|
||||
try
|
||||
{
|
||||
this.statsReportChannelId = Long.parseLong(statsReportChannelId);
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
this.statsReportChannelId = 0L;
|
||||
}
|
||||
this.statsReportFrequency = statsReportFrequency == null ? StatsReportFrequency.OFF : statsReportFrequency;
|
||||
}
|
||||
|
||||
public Settings(SettingsManager manager, long textId, long voiceId, long roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType)
|
||||
{
|
||||
this(manager, textId, voiceId, roleId, volume, defaultPlaylist, repeatMode, prefix, skipRatio, queueType, 0L, StatsReportFrequency.OFF);
|
||||
}
|
||||
|
||||
public Settings(SettingsManager manager, long textId, long voiceId, long roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType, long statsReportChannelId, StatsReportFrequency statsReportFrequency)
|
||||
{
|
||||
this.manager = manager;
|
||||
this.textId = textId;
|
||||
@@ -87,6 +108,8 @@ public class Settings implements GuildSettingsProvider
|
||||
this.prefix = prefix;
|
||||
this.skipRatio = skipRatio;
|
||||
this.queueType = queueType;
|
||||
this.statsReportChannelId = statsReportChannelId;
|
||||
this.statsReportFrequency = statsReportFrequency == null ? StatsReportFrequency.OFF : statsReportFrequency;
|
||||
}
|
||||
|
||||
// Getters
|
||||
@@ -135,6 +158,21 @@ public class Settings implements GuildSettingsProvider
|
||||
return queueType;
|
||||
}
|
||||
|
||||
public TextChannel getStatsReportChannel(Guild guild)
|
||||
{
|
||||
return guild == null ? null : guild.getTextChannelById(statsReportChannelId);
|
||||
}
|
||||
|
||||
public long getStatsReportChannelId()
|
||||
{
|
||||
return statsReportChannelId;
|
||||
}
|
||||
|
||||
public StatsReportFrequency getStatsReportFrequency()
|
||||
{
|
||||
return statsReportFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getPrefixes()
|
||||
{
|
||||
@@ -195,4 +233,16 @@ public class Settings implements GuildSettingsProvider
|
||||
this.queueType = queueType;
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setStatsReportChannel(TextChannel channel)
|
||||
{
|
||||
this.statsReportChannelId = channel == null ? 0L : channel.getIdLong();
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setStatsReportFrequency(StatsReportFrequency frequency)
|
||||
{
|
||||
this.statsReportFrequency = frequency == null ? StatsReportFrequency.OFF : frequency;
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,9 @@ public class SettingsManager implements GuildSettingsManager<Settings>
|
||||
o.has("repeat_mode") ? o.getEnum(RepeatMode.class, "repeat_mode"): RepeatMode.OFF,
|
||||
o.has("prefix") ? o.getString("prefix") : null,
|
||||
o.has("skip_ratio") ? o.getDouble("skip_ratio") : -1,
|
||||
o.has("queue_type") ? o.getEnum(QueueType.class, "queue_type") : QueueType.FAIR));
|
||||
o.has("queue_type") ? o.getEnum(QueueType.class, "queue_type") : QueueType.FAIR,
|
||||
o.has("stats_report_channel_id") ? o.getString("stats_report_channel_id") : null,
|
||||
o.has("stats_report_frequency") ? StatsReportFrequency.parse(o.getString("stats_report_frequency")) : StatsReportFrequency.OFF));
|
||||
});
|
||||
} catch (NoSuchFileException e) {
|
||||
// create an empty json file
|
||||
@@ -97,7 +99,7 @@ public class SettingsManager implements GuildSettingsManager<Settings>
|
||||
|
||||
private Settings createDefaultSettings()
|
||||
{
|
||||
return new Settings(this, 0, 0, 0, 100, null, RepeatMode.OFF, null, -1, QueueType.FAIR);
|
||||
return new Settings(this, 0, 0, 0, 100, null, RepeatMode.OFF, null, -1, QueueType.FAIR, 0L, StatsReportFrequency.OFF);
|
||||
}
|
||||
|
||||
protected void writeSettings()
|
||||
@@ -124,6 +126,10 @@ public class SettingsManager implements GuildSettingsManager<Settings>
|
||||
o.put("skip_ratio", s.getSkipRatio());
|
||||
if(s.getQueueType() != QueueType.FAIR)
|
||||
o.put("queue_type", s.getQueueType().name());
|
||||
if(s.getStatsReportChannelId() != 0L)
|
||||
o.put("stats_report_channel_id", Long.toString(s.getStatsReportChannelId()));
|
||||
if(s.getStatsReportFrequency() != StatsReportFrequency.OFF)
|
||||
o.put("stats_report_frequency", s.getStatsReportFrequency().name());
|
||||
obj.put(Long.toString(key), o);
|
||||
});
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.settings;
|
||||
|
||||
public enum StatsReportFrequency
|
||||
{
|
||||
OFF,
|
||||
MONTHLY,
|
||||
YEARLY;
|
||||
|
||||
public static StatsReportFrequency parse(String value)
|
||||
{
|
||||
if(value == null)
|
||||
return OFF;
|
||||
for(StatsReportFrequency frequency : values())
|
||||
if(frequency.name().equalsIgnoreCase(value))
|
||||
return frequency;
|
||||
return OFF;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.jagrosh.jmusicbot.utils;
|
||||
|
||||
import com.jagrosh.jdautilities.commons.JDAUtilitiesInfo;
|
||||
import com.jagrosh.jmusicbot.JMusicBot;
|
||||
import com.jagrosh.jmusicbot.entities.Prompt;
|
||||
import java.io.*;
|
||||
@@ -23,6 +24,7 @@ import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
@@ -186,6 +188,26 @@ public class OtherUtil
|
||||
else
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
public static String getCommandLibraryVersion()
|
||||
{
|
||||
try(InputStream input = OtherUtil.class.getResourceAsStream("/com/jagrosh/jmusicbot/versions.properties"))
|
||||
{
|
||||
if(input != null)
|
||||
{
|
||||
Properties properties = new Properties();
|
||||
properties.load(input);
|
||||
String version = properties.getProperty("jda-chewtils.version");
|
||||
if(version != null && !version.trim().isEmpty())
|
||||
return version.trim();
|
||||
}
|
||||
}
|
||||
catch(IOException ignored) {}
|
||||
|
||||
if(JDAUtilitiesInfo.VERSION_REVISION != null && !JDAUtilitiesInfo.VERSION_REVISION.startsWith("@"))
|
||||
return JDAUtilitiesInfo.VERSION;
|
||||
return JDAUtilitiesInfo.VERSION_MAJOR + "." + JDAUtilitiesInfo.VERSION_MINOR;
|
||||
}
|
||||
|
||||
public static String getLatestVersion()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.jagrosh.jmusicbot;
|
||||
|
||||
import com.jagrosh.jmusicbot.datalog.DataLogService;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsAccess;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsFilters;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsMessageFormatter;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsSummary;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsTimeRange;
|
||||
import com.jagrosh.jmusicbot.datalog.StatsRow;
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class StatsReportingTest
|
||||
{
|
||||
@Test
|
||||
public void parsesRangesAndSources()
|
||||
{
|
||||
assertEquals("all", StatsTimeRange.parse("all").getKey());
|
||||
assertEquals("today", StatsTimeRange.parse("today").getKey());
|
||||
assertEquals("week", StatsTimeRange.parse("week").getKey());
|
||||
assertEquals("month", StatsTimeRange.parse("month").getKey());
|
||||
assertEquals("year", StatsTimeRange.parse("year").getKey());
|
||||
assertEquals("60d", StatsTimeRange.parse("60d").getKey());
|
||||
assertEquals("1y", StatsTimeRange.parse("1y").getKey());
|
||||
assertEquals("2026-04", StatsTimeRange.month(2026, 4).getKey());
|
||||
assertEquals("2025", StatsTimeRange.year(2025).getKey());
|
||||
assertNull(StatsTimeRange.month(2026, 13));
|
||||
assertNull(StatsTimeRange.parse("forever-ish"));
|
||||
|
||||
assertEquals("youtube", StatsFilters.normalizeSource("yt"));
|
||||
assertEquals("soundcloud", StatsFilters.normalizeSource("SC"));
|
||||
assertNull(StatsFilters.normalizeSource("all"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userStatsRespectSelfOrAdminAccess()
|
||||
{
|
||||
assertTrue(StatsAccess.canViewUserStats(1L, 1L, false));
|
||||
assertTrue(StatsAccess.canViewUserStats(1L, 2L, true));
|
||||
assertFalse(StatsAccess.canViewUserStats(1L, 2L, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkedFieldsStayWithinDiscordLimit()
|
||||
{
|
||||
List<StatsRow> rows = Arrays.asList(
|
||||
new StatsRow(1L, repeat("Very Long Track Name ", 20), "Artist", 10L, 0L),
|
||||
new StatsRow(2L, repeat("Another Long Track Name ", 20), "Artist", 9L, 0L),
|
||||
new StatsRow(3L, repeat("Third Long Track Name ", 20), "Artist", 8L, 0L));
|
||||
EmbedBuilder builder = new EmbedBuilder();
|
||||
StatsMessageFormatter.addChunkedField(builder, "Tracks", StatsMessageFormatter.countList(rows, "plays", 3), false);
|
||||
MessageEmbed embed = builder.build();
|
||||
assertTrue(embed.getFields().size() > 1);
|
||||
for(MessageEmbed.Field field : embed.getFields())
|
||||
assertTrue(field.getValue().length() <= 1024);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregatesPlaybackStatsFromSqlite() throws Exception
|
||||
{
|
||||
String dbName = "stats-test-" + System.nanoTime();
|
||||
DataLogService service = new DataLogService(null, "jdbc:sqlite:file:" + dbName + "?mode=memory&cache=shared", "", "");
|
||||
try
|
||||
{
|
||||
service.getStatsSummary(1L, StatsTimeRange.all(), null, null);
|
||||
Connection connection = getConnection(service);
|
||||
seedStatsData(connection);
|
||||
|
||||
StatsSummary server = service.getStatsSummary(1L, StatsTimeRange.all(), null, null);
|
||||
assertEquals(5L, server.getTotalPlays());
|
||||
assertEquals(305000L, server.getTotalListeningMillis());
|
||||
assertEquals("Song A", server.getTopTracks().get(0).getLabel());
|
||||
assertEquals(3L, server.getTopTracks().get(0).getCount());
|
||||
assertEquals("youtube", server.getTopSources().get(0).getLabel());
|
||||
assertEquals(2L, server.getSkipVotes().get(0).getCount());
|
||||
assertEquals(1L, server.getSkipActions().get(0).getCount());
|
||||
|
||||
StatsSummary user = service.getStatsSummary(1L, StatsTimeRange.all(), null, 100L);
|
||||
assertEquals(2L, user.getTotalPlays());
|
||||
assertEquals(120000L, user.getTotalListeningMillis());
|
||||
|
||||
StatsSummary soundcloud = service.getStatsSummary(1L, StatsTimeRange.all(), "soundcloud", null);
|
||||
assertEquals(2L, soundcloud.getTotalPlays());
|
||||
assertEquals("Song B", soundcloud.getTopTracks().get(0).getLabel());
|
||||
|
||||
List<StatsRow> requestedByAlice = service.getTopRequestedTracks(1L, StatsTimeRange.all(), null, java.util.Collections.singletonList(100L), 5);
|
||||
assertEquals(1, requestedByAlice.size());
|
||||
assertEquals("Song A", requestedByAlice.get(0).getLabel());
|
||||
assertEquals(2L, requestedByAlice.get(0).getCount());
|
||||
|
||||
List<StatsRow> listenedByBob = service.getTopListenedTracks(1L, StatsTimeRange.all(), null, java.util.Collections.singletonList(200L), 5);
|
||||
assertEquals("Song A", listenedByBob.get(0).getLabel());
|
||||
assertEquals(150000L, listenedByBob.get(0).getMillis());
|
||||
|
||||
List<StatsRow> profileMatches = service.findUsersByProfile(1L, "ali", 5);
|
||||
assertEquals(1, profileMatches.size());
|
||||
assertEquals(100L, profileMatches.get(0).getId());
|
||||
}
|
||||
finally
|
||||
{
|
||||
service.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private Connection getConnection(DataLogService service) throws Exception
|
||||
{
|
||||
Field field = DataLogService.class.getDeclaredField("connection");
|
||||
field.setAccessible(true);
|
||||
return (Connection)field.get(service);
|
||||
}
|
||||
|
||||
private void seedStatsData(Connection connection) throws Exception
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
try(Statement st = connection.createStatement())
|
||||
{
|
||||
st.execute("INSERT INTO guilds (guild_id, name, first_seen_at) VALUES (1, 'Guild', " + now + ")");
|
||||
st.execute("INSERT INTO users (user_id) VALUES (100)");
|
||||
st.execute("INSERT INTO users (user_id) VALUES (200)");
|
||||
st.execute("INSERT INTO user_profiles (user_id, guild_id, username, seen_at) VALUES (100, 1, 'Alice', " + now + ")");
|
||||
st.execute("INSERT INTO user_profiles (user_id, guild_id, username, seen_at) VALUES (200, 1, 'Bob', " + now + ")");
|
||||
st.execute("INSERT INTO tracks (id, source, identifier, uri, title, author, length_ms, is_stream) VALUES (1, 'youtube', 'a', 'https://youtu.be/a', 'Song A', 'Artist A', 120000, 0)");
|
||||
st.execute("INSERT INTO tracks (id, source, identifier, uri, title, author, length_ms, is_stream) VALUES (2, 'soundcloud', 'b', 'https://soundcloud.com/b', 'Song B', 'Artist B', 180000, 0)");
|
||||
}
|
||||
try(PreparedStatement ps = connection.prepareStatement("INSERT INTO play_sessions (id, guild_id, track_id, requested_by, started_at, ended_at, duration_ms, listeners_at_start) VALUES (?,1,?,?,?,?,?,?)"))
|
||||
{
|
||||
insertPlay(ps, 1, 1, 100, now - 300000, now - 180000, 120000, 2);
|
||||
insertPlay(ps, 2, 1, 100, now - 170000, now - 50000, 120000, 1);
|
||||
insertPlay(ps, 3, 2, 200, now - 40000, now - 10000, 30000, 1);
|
||||
insertPlay(ps, 4, 2, 200, now - 10000, now - 5000, 5000, 1);
|
||||
insertPlay(ps, 5, 1, 200, now - 1000000000L, 0L, 0L, 1);
|
||||
}
|
||||
try(PreparedStatement ps = connection.prepareStatement("INSERT INTO listener_sessions (play_session_id, user_id, joined_at, left_at) VALUES (?,?,?,?)"))
|
||||
{
|
||||
insertListener(ps, 1, 100, now - 300000, now - 240000);
|
||||
insertListener(ps, 2, 100, now - 170000, now - 110000);
|
||||
insertListener(ps, 3, 200, now - 40000, now - 10000);
|
||||
insertListener(ps, 1, 200, now - 300000, now - 270000);
|
||||
insertListener(ps, 4, 200, now - 20000, 0L);
|
||||
insertListener(ps, 5, 200, now - 1000000000L, 0L);
|
||||
}
|
||||
try(PreparedStatement ps = connection.prepareStatement("INSERT INTO queue_events (guild_id, user_id, track_id, event_type, created_at) VALUES (1,?,?,?,?)"))
|
||||
{
|
||||
insertQueueEvent(ps, 100, 1, "SKIP_VOTE", now - 100000);
|
||||
insertQueueEvent(ps, 100, 1, "SKIP_VOTE", now - 90000);
|
||||
insertQueueEvent(ps, 200, 1, "SKIP_RATIO_MET", now - 80000);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertPlay(PreparedStatement ps, long id, long trackId, long requestedBy, long startedAt, long endedAt, long duration, int listeners) throws Exception
|
||||
{
|
||||
ps.setLong(1, id);
|
||||
ps.setLong(2, trackId);
|
||||
ps.setLong(3, requestedBy);
|
||||
ps.setLong(4, startedAt);
|
||||
if(endedAt == 0L)
|
||||
ps.setNull(5, java.sql.Types.BIGINT);
|
||||
else
|
||||
ps.setLong(5, endedAt);
|
||||
if(duration == 0L)
|
||||
ps.setNull(6, java.sql.Types.BIGINT);
|
||||
else
|
||||
ps.setLong(6, duration);
|
||||
ps.setInt(7, listeners);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
private void insertListener(PreparedStatement ps, long sessionId, long userId, long joinedAt, long leftAt) throws Exception
|
||||
{
|
||||
ps.setLong(1, sessionId);
|
||||
ps.setLong(2, userId);
|
||||
ps.setLong(3, joinedAt);
|
||||
if(leftAt == 0L)
|
||||
ps.setNull(4, java.sql.Types.BIGINT);
|
||||
else
|
||||
ps.setLong(4, leftAt);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
private void insertQueueEvent(PreparedStatement ps, long userId, long trackId, String eventType, long createdAt) throws Exception
|
||||
{
|
||||
ps.setLong(1, userId);
|
||||
ps.setLong(2, trackId);
|
||||
ps.setString(3, eventType);
|
||||
ps.setLong(4, createdAt);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
private String repeat(String value, int count)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for(int i = 0; i < count; i++)
|
||||
builder.append(value);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user