Compare commits

...
151 Commits
Author SHA1 Message Date
Christian F. Brinkmann (cloud) c25cda721a feat: implement playback reporting and statistics command (v0.6.6)
- Added DataLogService queries for server and user statistics.
- Implemented 'stats' command with support for time ranges and filters.
- Added StatsReportScheduler for periodic (daily/weekly/monthly) reports.
- Integrated play count display into the 'nowplaying' embed.
- Updated settings to support stats report channel and frequency configuration.
- Added documentation for analytics in setup.md and docker.md.
- Bumped version to 0.6.6 in pom.xml.
2026-05-17 09:41:14 +02:00
Christian F. Brinkmann (cloud) f95b50af89 Bump version to 0.6.5 and update YouTube common dependency to 1.18.1 2026-05-10 23:22:18 +02:00
Christian F. Brinkmann (cloud) 1e6904d329 Fix Chewtils version in about/debug output
Work around the broken VERSION_REVISION placeholder in jda-chewtils 2.2.1.

- render the about embed from LoggedAboutCmd
- load the Chewtils version from a filtered project resource
- use the same resolved version in DebugCmd
2026-04-08 16:22:18 +02:00
Christian F. Brinkmann (cloud) 5ff7b2f28e Update README and add setup, docker, and changelog documentation 2026-03-30 14:31:45 +02:00
Christian F. Brinkmann (cloud) 28dfdcf680 Add playback analytics logging and bump version to 0.6.3
Add JDBC-backed activity logging for queue, playback, search, skip, listener, and command events.

Logging is disabled by default and can be enabled with `dblog.enabled=true`. The default JDBC URL uses SQLite via `jdbc:sqlite:musicbot.db`, but other JDBC backends can be configured with the same settings block.
2026-03-30 14:11:25 +02:00
Christian F. Brinkmann (cloud) c5073b8755 Bump version to 0.6.2
Fix status message repost races and add a config toggle to disable the feature.

Add the statusmessages config option in reference.conf. When enabled, the handler now uses stronger synchronization and safer message-id reset behavior during reposts and edit failures.
2026-03-30 14:09:52 +02:00
Christian F. Brinkmann (cloud) 5501f02a97 Bump version to 0.6.1 2026-03-12 16:23:41 +01:00
Christian F. Brinkmann (cloud) 66e207eabe Add StatusMessageHandler to manage playback status messages and update Bot and Listener classes 2026-03-12 16:23:12 +01:00
Christian F. Brinkmann (cloud) db3a57d517 Update .gitignore to exclude all hidden files and ensure proper formatting 2026-03-12 15:28:38 +01:00
Christian F. Brinkmann 81fa3fed25 Bump version to 0.6.0 2026-03-06 10:15:59 +01:00
Christian F. Brinkmann 9484c373ff Downgrade jsoup dependency version from 1.15.3 to 1.14.2 2026-03-06 09:32:02 +01:00
Christian F. Brinkmann 860822d520 Update JLyrics dependency version from master-SNAPSHOT to master 2026-03-05 15:40:27 +01:00
Christian F. Brinkmann a0c313047f Add DAVE protocol support and update dependencies for JDA 6.3+ 2026-03-05 15:26:25 +01:00
Christian F. Brinkmann db69ea6787 Update Docker Hub link in README 2025-11-15 23:56:45 +01:00
Christian F. Brinkmann 5dba2c355b Bump version from 0.5.1 to 0.5.2 in pom.xml 2025-11-15 12:57:11 +01:00
Christian F. Brinkmann 9fd246ac25 Fix JDA-Chewtils dependency groupId and update version to jda-6.1.1-SNAPSHOT 2025-11-15 12:28:38 +01:00
Christian F. Brinkmann 6a15bbb0df Add MESSAGE_CONTENT and additional intents to JMusicBot 2025-11-15 12:28:23 +01:00
Christian F. Brinkmann 16bc176685 Bump version to 0.5.1 and enable additional intents for JDA 2025-11-15 10:56:43 +01:00
Christian F. Brinkmann 50e8bff831 Bump version from 0.4.12 to 0.5.0 in pom.xml 2025-11-14 21:49:41 +01:00
Christian F. Brinkmann 9c012eb169 Update README to reflect JDA 6 upgrade and MESSAGE_CONTENT intent requirements 2025-11-14 21:49:33 +01:00
Christian F. Brinkmann 87771df1fb Update Lavaplayer and Lavalink dependencies to latest versions 2025-11-14 21:49:04 +01:00
Christian F. Brinkmann 5d35f5bb4c Enable MESSAGE_CONTENT intent for JDA
Appears to be a requirement for JDA 5 or later
2025-11-14 21:13:10 +01:00
Christian F. Brinkmann ecd8cdf77b Update logback-classic dependency to version 1.5.21 2025-11-14 20:29:33 +01:00
Christian F. Brinkmann 39e5bf7786 Upgrade JDA from 4.4.1_353 to 6.1.1
- Updated JDA dependency to 6.1.1
- Switched to Chew's JDA-Chewtils fork (fix/components-v2 branch) for JDA v6 support
- Fixed breaking API changes:
  - Channel types moved to channel.concrete package
  - MessageBuilder → MessageCreateBuilder/MessageCreateData
  - Events moved to events.session package
  - GuildMessageDeleteEvent → MessageDeleteEvent
  - Permissions: MESSAGE_READ → VIEW_CHANNEL, MESSAGE_WRITE → MESSAGE_SEND
  - Methods: inVoiceChannel() → inAudioChannel()
  - File uploads: sendFile() → sendFiles() with FileUpload
  - Removed CacheFlag.EMOTE
  - Removed LoginException handling (no longer thrown by JDABuilder)

Note: LoginException removal means less helpful error messages for invalid tokens.
2025-11-14 14:12:21 +01:00
Christian F. Brinkmann 650a9ea996 bump version to 0.4.12 and update Lavalink dependency to 1.13.2 2025-06-08 21:01:45 +02:00
Christian F. Brinkmann 0e0c05acf9 Update version to 0.4.11-1 and add Lavalink snapshot repository 2025-05-01 09:24:57 +02:00
Christian F. Brinkmann 2e52ecac3b bump version to 0.4.11 and update lavalink to 1.13.0 2025-05-01 01:07:01 +02:00
Christian F. Brinkmann (cloud) 450ee7acb8 bump lavalink to 1.12.0, bump version to 4.10 2025-03-28 01:16:49 +01:00
Christian F. Brinkmann a7b56b67ab Update README.md to mention nssm for windows 2025-02-26 23:39:18 +01:00
Christian F. Brinkmann fedb97e1c7 Update README.md 2025-02-26 16:17:21 +01:00
Christian F. Brinkmann 68f951dc63 Merge remote-tracking branch 'origin/master' 2025-02-26 15:46:45 +01:00
Christian F. Brinkmann 01eee8813a bump project version to 0.4.9 2025-02-26 15:46:07 +01:00
Christian F. Brinkmann 3df09a4cd2 bump lavaplayer common dependency version to 1.11.5 2025-02-26 15:45:24 +01:00
Christian F. Brinkmann 0084ba7baf Update README.md
Add demo server discord invite
2025-02-19 11:16:26 +01:00
Christian F. Brinkmann 35e756df9b bump project version to 0.4.8 2025-02-13 14:20:38 +01:00
Christian F. Brinkmann 426e28ed8c bump lavaplayer dependencies to latest versions 2025-02-13 14:16:16 +01:00
Christian F. Brinkmann dd72baca0d Reflect recent changes in the readme 2025-01-11 13:45:31 +01:00
Christian F. Brinkmann ac4e09cbd9 bump project patch version 2025-01-11 13:41:38 +01:00
Christian F. Brinkmann fd01d94cf2 bump lavaplayer version 2025-01-11 13:40:48 +01:00
Christian F. Brinkmann 95f72937c8 Merge remote-tracking branch 'other-fork/fix-youtube' 2025-01-11 13:38:53 +01:00
Michaili K 792cb7a8b7 Remove whitespace 2025-01-03 00:02:27 +01:00
Michaili K 03ba83f06b Log the entire exception in debug for brevity 2025-01-03 00:00:58 +01:00
Michaili K 3b3a483b17 Use Selenium with Chrome to obtain PO token and visitordata 2025-01-02 23:25:44 +01:00
Christian F. Brinkmann 1874252ff6 change urls for update check logic 2024-12-14 13:26:33 +01:00
Christian F. Brinkmann aa8e430ff1 bump version to 0.4.6 2024-12-14 13:26:17 +01:00
Christian F. Brinkmann 204bb6f04a bump lavalink version in the hopes that it fixes some errors 2024-12-14 13:05:20 +01:00
Michaili K 9ec21bf329 Update youtube-source to 1.11.1 2024-12-10 14:16:24 +01:00
Christian F. Brinkmann b419191bad use actual version instead of snapshot in pom.xml 2024-12-06 18:17:37 +01:00
Christian F. Brinkmann aa89145ae8 update readme to reflect that i have now made miniscule dependency changes myself 2024-12-06 18:11:20 +01:00
Christian F. Brinkmann 293733f0e7 bump versions of lavaplayer and lavalink to most recent in the hopes that this fixes something 2024-12-06 17:55:44 +01:00
Michaili K 7be02b1fe2 Add quotes around po token & visitor data config 2024-11-21 04:12:18 +01:00
Michaili K 1ac46d9ec5 Implement IP rotation for YouTube 2024-11-21 03:47:12 +01:00
Michaili K 026ce87440 Implement PO Tokens for YouTube playback 2024-11-21 01:41:13 +01:00
Michaili K e5f34cc900 Update deps 2024-11-21 01:25:57 +01:00
Christian F. Brinkmann c9a2477f86 Update README.md 2024-10-25 13:26:06 +02:00
root 61a18a1db7 update README to give some info about the fork 2024-10-24 17:24:38 +02:00
root 7fb44e296b Merge branch 'pr-1670', use 1.8.3 instead of 1.8.2 2024-10-24 14:01:24 +02:00
Lythus 4ae9f05776 ignore dockerfile 2024-10-19 21:28:44 +08:00
Lythus 84facb8b82 updated lavalink version to 1.8.3 2024-10-19 20:36:53 +08:00
Michaili K 070938b326 Add anothen message to onTrackError handler 2024-10-15 10:54:27 +02:00
Michaili K 58a9e76edb Formatting changes 2024-10-11 23:42:06 +02:00
Michaili K 96ffcd8291 Handle specific track exceptions with a special message 2024-10-10 20:00:40 +02:00
Michaili K 844a1bb75a Change youtubeoauth2 option description 2024-10-10 19:11:29 +02:00
Michaili K 6094e49ab2 Return early when failing to read the token file 2024-10-10 19:09:05 +02:00
Michaili K 88ce3e1149 Stop logging the refresh token on startup 2024-10-10 19:08:49 +02:00
Michaili K 463bf75783 Prepend "Google" to "accounts" in OAuth2 DM for clarification 2024-10-06 04:56:00 +02:00
Michaili K 348fc3ceec DM the user when the YoutubeOauth2Handler is providing us a token 2024-10-02 21:39:29 +02:00
Lythus 30eafb9239 Updated JDA-Utilities 2024-09-30 15:22:32 +08:00
Michaili K c95f2248f3 Bump youtube-source to 1.8.2 2024-09-26 02:12:38 +02:00
Michail b80b0c8459 Remove accidental HTML tag :) 2024-08-24 14:01:15 +02:00
Michail 4a8392fdb9 Catch potential exceptions thrown by the useOAuth2 call 2024-08-24 13:38:52 +02:00
Michail 9d6584525c Implement YouTube OAuth2 login 2024-08-24 13:25:11 +02:00
Michail 909b655f5c Update YouTube source manager 2024-08-24 12:09:43 +02:00
Michaili K 859e5c5862 Update lavaplayer youtube source to 1.5.2 (#1655) 2024-08-12 13:28:38 -04:00
njezi 43b63f1ba8 fixed a mistake (#1648) 2024-08-06 15:59:19 -04:00
John Grosh eca2563abf fix npe and an import (#1567)
* fix npe and an import

* stop double posting

* switched stacktrace to log, changed some logs
2024-08-01 09:42:17 -04:00
kokofixcomputersandMichaili K e6bfd1841f fix error when playing (#1632)
* fix error when playing

* Update youtube source to 1.5.1

---------

Co-authored-by: Michaili K <git@michaili.dev>
2024-08-01 09:14:51 -04:00
Timothy Gillespie f140b41bf5 Use shell comparsion (#1627) 2024-08-01 09:10:04 -04:00
John Grosh 8733899aa0 Update bug-report.yml 2024-07-10 17:35:50 -04:00
Moritz Bender e5fd05e601 Update dependencies to fix youtube issues (#1609) 2024-07-10 13:19:13 +02:00
John Grosh 6a5a9c7a75 Change playlist page count (#1542)
* change playlist page count

* make the value configurable

* small refactor
2024-05-10 17:43:49 -04:00
Michaili K 8557f7ab8e Log track exceptions in the audio handler (#1558) 2024-05-10 15:36:43 -04:00
Michaili K 48e62f1a50 Revert "Always self-deafen (#1491)" (#1551)
This reverts commit a7807b96e2.
2024-05-10 15:32:30 -04:00
John Grosh 6f12c33781 custom eval engine (#1530) 2024-05-10 15:23:30 -04:00
Michaili K 0afb3db269 Switch to lavalink's new YouTube audio source manager (#1552)
* Switch to new lavalink's new YouTube audio source manager

* Add back setPlaylistPageCount call for the youtube audio source manager

* Manually add all the individual audio sources EXCEPT for old youtube

* Remove unused import

* Upgrade lavaplayer-youtube-source to 1.0.3

* Upgrade lavaplayer-youtube-source to 1.0.4
2024-05-10 14:19:11 -04:00
Michaili KandWhew., Inc 81322ef993 Add seek command (supersedes #344) (#674)
* Add seek command

* Combine nested if statements

* Put the seek command in correct place to keep alphabetical order

* Add license header

* Brackets on next line

* Restructure if-statements

* Check for permissions with DJCommand#checkDJPermission

* Make regex slightly smaller

* Optimize imports

* Output length of current track if requested seek time is invalid

* Restate seeked point when seeked successfully

* Add empty newline at end of file to keep consistency

* Create TimeUtil class for parsing and formatting time

* Move FormatUtil#formatTime to TimeUtil, and refactor

* Apply requested changes (Pass 2)

* Seek based on current position in track

* Apply requested changes (Pass 3)

* Add javadoc param

* Apply requested changes (Pass 4)

* Fix merge

* Avoid reassigning parameter (Codacy)

* Rework timestamp parsing

* Refactor timestamp parsing

* Apply requested changes (Pass 5)

* Add examples in help

* Apply requested changes (Pass 6)

* Fix missing import

* Keep track of start timestamp & add "unit" times

* Fix my abdominal merge with QueuedTrack

* Use RequestMetadata to store start timestamp

* Store request info in request metadata

* Add regex to try getting a timestamp from the url

* Require RequestMetadata for QueuedTracks

* Add some unit tests for unit seeking

* Add docs & examples

---------

Co-authored-by: Whew., Inc <22574706+Whew-Inc@users.noreply.github.com>
2024-05-10 14:14:28 -04:00
Michaili KandJohn Grosh b3ffbdba7d Add log level configuration (#1156)
* Add loglevel config

* Change order of fields

---------

Co-authored-by: John Grosh <john.a.grosh@gmail.com>
2024-03-30 15:34:46 -04:00
John Grosh 801b3769a1 Fix startup message type and add URL to message (#1521)
* fix

* add URL to error message
2024-03-30 15:18:52 -04:00
Michaili Kandunknown 49c3ec7960 Switch to lavalink's lavaplayer fork (#1490)
* Switch to lavalink's lavaplayer fork

* Create logback turbofilter to suppress YouTube auth tokens warning

* added license header

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-04 19:38:04 -05:00
John Grosh 6752695b43 rename github action file and update readme (#1494) 2024-03-04 19:35:32 -05:00
John Grosh bb17c984a2 Codefactor Fixes (#1493)
* codefactor fixes

* anotha one
2024-03-04 19:35:17 -05:00
Michaili K a7807b96e2 Always self-deafen (#1491) 2024-03-04 19:08:27 -05:00
2e9dd5df49 add LinearQueue (#1194)
* add LinearQueue

* rework queueType to be server-specific

* rework QueueType enum and command

* add QueueType supplier

* fix queue type formatting

* add QueueSupplier util

* fix code issues

* Fix unit tests

* add suggested changes on PR

---------

Co-authored-by: Michaili K <mysteriouscursor+git@protonmail.com>
Co-authored-by: Michail <git@michaili.dev>
2024-03-04 18:49:01 -05:00
Maoyue 貓月andunknown 1ad16181d0 Support new Discord username (#1397)
* Support new username

* Support new username, again

* Make a separate username formatting function

* Add some missing usage

* switch away from using static imports, add shortcuts for jda User and metadata UserInfo, add null check

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-02 19:39:16 -05:00
John Grosh bb53bad1ff Update test.yml 2024-03-01 20:11:30 -05:00
Kevin S 8a1066cc87 Allow configurable default skip ratio (#1140)
* Allow configurable default skip ratio

Revert "Allow configurable default skip ratio"

This reverts commit 10e38b4132ee2b28fb583603d69e6228da211ecd.

* FIX: Use -1 to denote a guild has not set a skip ratio, and opted to use the bot default.
2024-03-01 19:42:39 -05:00
Michaili Kandunknown 23c2dfa78e Make JMusicBot error & shut down if ran in an unsupported manner (#1486)
* Make JMusicBot error & shut down if ran in an unsupported manner

* add info about message content intent

* five second rule

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-01 19:26:58 -05:00
Michaili Kandunknown 18bf6ac0f6 Remove channel topic info (#1485)
* Remove channel topic info

As Discord has severely increased the ratelimits for updating the channel topic, updating the channel topic has become not viable for bots.

* fix compilation error

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-01 19:19:18 -05:00
John Grosh fd6de60fc7 Merge pull request #1489 from jagrosh/add-more-sources
Add more sources and check more nulls
2024-03-01 19:00:50 -05:00
unknown 4c8ff655d8 update github actions to jdk 11 2024-03-01 18:26:10 -05:00
John Grosh ac8661c904 Merge pull request #1487 from MichailiK/fix/server-settings-logging
Don't warn about failing to load server settings when the file isn't found
2024-03-01 18:24:09 -05:00
unknown 4f926baf76 update circleci settings 2024-03-01 18:08:46 -05:00
unknown c5ecf89852 switch to java 11 2024-03-01 18:04:26 -05:00
unknown a52103620a update version 2024-03-01 18:00:02 -05:00
unknown fa5e33e2a5 fix formatting and nulls in metadata 2024-03-01 17:53:08 -05:00
unknown 781a94ea91 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2024-03-01 17:50:39 -05:00
Michail ff1ede9d12 Log "loaded config" message again after successfully initializing the GUI 2024-02-28 21:33:45 +01:00
Michail 90d9a1631a Create a new serversettings.json file if one doesn't exist 2024-02-28 21:33:10 +01:00
John Grosh c4502bbe16 Merge pull request #1460 from jagrosh/dependabot/maven/ch.qos.logback-logback-classic-1.2.13
Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13
2024-02-28 15:13:45 -05:00
Michail 2d4f2671c9 Don't warn about failing to load server settings when the file isn't found 2024-02-28 21:01:08 +01:00
John Grosh e595de2033 Merge pull request #1479 from MichailiK/fix/eval-shenanigans
Fix eval command failing in DMs
2024-02-10 17:28:33 -05:00
Michail 53875f2791 Properly fix eval command failing in DMs 2024-02-10 23:25:11 +01:00
Michail cd7e643228 Possible workaround for Eval command failing 2024-02-10 23:10:07 +01:00
dependabot[bot] 0e1f0d3bfa Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.2.3 to 1.2.13.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.2.3...v_1.2.13)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-05 22:17:47 +00:00
John Grosh 2c76164f6d Merge pull request #1304 from LachlanPond/master
Catch JDA exception when there is no internet connection
2023-03-06 16:31:28 -05:00
Lachlan Pond 788cd40f2c Added import for ErrorResponseException 2023-03-04 13:59:42 +10:00
Michaili K 1e12e4e01d Bump JDA to 4.4.1_353 2023-03-01 20:52:53 +01:00
Lachlan Pond c4cbfe2a11 Catch JDA exception when there is no internet connection 2023-02-27 21:52:22 +10:00
John Grosh bbed9e8990 Merge pull request #1294 from lrbn86/master
Sanitize the playlist name
2023-02-14 12:22:51 -05:00
lrbn86 354a0b3def Sanitize the playlist name 2023-02-13 18:31:14 -05:00
John Grosh 2faef67b30 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2022-09-02 21:56:23 -04:00
John Grosh aee076c854 add more sources 2022-09-02 21:56:19 -04:00
John Grosh b76f6fa07c Merge pull request #1141 from t3rminus/fix-warnings-deprecations
Fix warnings and deprecations
2022-09-02 21:55:14 -04:00
John Grosh ec4e9e624c Merge pull request #1209 from jagrosh/dependabot/maven/org.jsoup-jsoup-1.15.3
Bump jsoup from 1.14.2 to 1.15.3
2022-09-02 21:53:47 -04:00
dependabot[bot] a7be2c4602 Bump jsoup from 1.14.2 to 1.15.3
Bumps [jsoup](https://github.com/jhy/jsoup) from 1.14.2 to 1.15.3.
- [Release notes](https://github.com/jhy/jsoup/releases)
- [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES)
- [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.14.2...jsoup-1.15.3)

---
updated-dependencies:
- dependency-name: org.jsoup:jsoup
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 23:43:19 +00:00
John Grosh 6007d3e99b Merge pull request #1192 from yo-louk/queue-links
Queue entry formatting fix
2022-07-07 16:00:25 -04:00
yo-louk a17b2f20ee check uri type before formatting queue entry 2022-07-07 21:26:18 +02:00
John Grosh 4375d4caa8 Merge pull request #1155 from MichailiK/pr/ci/simplify-release
Update generated description in release workflow
2022-04-09 15:01:04 -04:00
Michaili K c22807bcbb Update make-release.yml 2022-04-09 18:25:01 +02:00
John Grosh baf629a910 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2022-04-06 18:28:07 -04:00
John Grosh c4e11d682e update deps, minor code cleanup 2022-04-06 18:27:59 -04:00
Xiao Quan 全笑andMichaili K 041ab27b94 Fixes #514 Require inputing name for playlist make <name> command (#1046)
* Fixes #514 Require inputing name for `playlist make <name>` command

playlist make command without a name will now not make an empty playlist without a playlist name.

* Update src/main/java/com/jagrosh/jmusicbot/commands/owner/PlaylistCmd.java

Co-authored-by: Michaili K <mysteriouscursor+git@protonmail.com>

* Rid excessive hints

Co-authored-by: Michaili K <mysteriouscursor+git@protonmail.com>
2022-04-06 12:44:52 -04:00
John Grosh 85aebe8a89 Update make-release.yml 2022-04-05 19:06:50 -04:00
John Grosh 3c306a8d7a Update make-release.yml 2022-04-05 19:04:50 -04:00
John Grosh cadaf94cf7 Update make-release.yml 2022-04-05 18:55:05 -04:00
John Grosh 15a47a7b4d Update make-release.yml 2022-04-05 18:51:36 -04:00
John Grosh 92fca22059 Update make-release.yml 2022-04-05 18:42:44 -04:00
John Grosh 0cdc0d2d34 Update make-release.yml 2022-04-05 18:29:06 -04:00
John Grosh 1d1dfe103e Create make-release.yml 2022-04-05 18:25:29 -04:00
John Grosh ab39d2cd48 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2022-04-05 17:47:09 -04:00
John Grosh 32d165d1b3 add natives to base repo because I don't think any other repo is opening up soon 2022-04-05 17:46:59 -04:00
Kevin S 8457f944cd Fix warnings and deprecations 2022-03-31 18:30:19 -07:00
John Grosh c404e92932 Create test.yml 2022-01-06 16:10:02 -05:00
John Grosh 6af86cae5d Update bug-report.yml 2022-01-06 10:24:11 -05:00
John Grosh 9421730229 Delete feature-request.md 2022-01-06 10:18:58 -05:00
John Grosh b76e31c7e2 Delete bug-report.md 2022-01-06 10:18:48 -05:00
John Grosh 6056ff2c13 Merge pull request #1032 from yo-louk/queue-links
Queue links
2021-11-13 14:14:21 -05:00
yo-louk 8c92a7a75a changed QueuedTrack class instead 2021-11-12 00:55:16 +01:00
yo-louk e942f3ef21 changed list format 2021-11-12 00:31:31 +01:00
John Grosh 157da6c0da Update config.yml 2021-10-04 17:36:01 -04:00
John Grosh 03b909a3f5 whoops 2021-10-04 08:09:12 -04:00
99 changed files with 6825 additions and 628 deletions

No files matched your search

+12 -1
View File
@@ -7,7 +7,7 @@ jobs:
build:
docker:
# specify the version you desire here
- image: circleci/openjdk:8u171-jdk
- image: cimg/openjdk:11.0
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
@@ -39,3 +39,14 @@ jobs:
# run tests!
- run: mvn integration-test
workflows:
version: 2
build:
jobs:
- build:
filters:
branches:
ignore:
- gh-pages
- gh-pages-generated
-29
View File
@@ -1,29 +0,0 @@
---
name: Bug Report
about: Report a bug or unexpected behavior
title: "[Bug Report] Title Here"
labels: ''
assignees: ''
---
**Describe the bug**
Replace this text with a clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
**Expected behavior**
Replace this text with a clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem here.
**Debug Info**
Please run the `debug` command and paste it here or upload it as an attachment. If you cannot run the command, please provide information about your system such as operating system version, JMusicBot version, etc.
**Additional context**
Replace this text with any other context about the problem.
+8 -2
View File
@@ -36,7 +36,6 @@ body:
attributes:
label: Debug Output
description: Please run the `debug` command and paste it here or upload it as an attachment. If you cannot run the command, please provide information about your system such as operating system version, JMusicBot version, etc.
render: shell
validations:
required: true
- type: textarea
@@ -54,5 +53,12 @@ body:
required: true
- label: I have searched for similar issues [on the issues page](https://github.com/jagrosh/MusicBot/issues?q=is%3Aissue)
required: true
- label: "I am running the latest version of the bot: ![Release](https://img.shields.io/github/release/jagrosh/MusicBot.svg)"
- label: I am using an official release from [the releases page](https://github.com/jagrosh/MusicBot/releases)
required: true
- type: input
id: version
attributes:
label: JMusicBot Version
description: Which version of JMusicBot are you running?
validations:
required: true
-27
View File
@@ -1,27 +0,0 @@
---
name: Feature Request
about: Suggest an idea for this project
title: "[Feature Request] Title Here"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
Replace this text with a clear and concise description of what the problem is.
**Describe the solution you'd like**
Replace this text with a clear and concise description of what you want to happen.
**Describe the basic flow/steps of using this feature**
How this feature would be used:
1.
2.
3.
**Additional context**
Add any other context or screenshots about the feature request here.
**Please check the following links to confirm that your feature has not already been requested:**
https://github.com/jagrosh/MusicBot/labels/enhancement
https://github.com/jagrosh/MusicBot/wiki/Things-That-Won%27t-Be-Added
+22
View File
@@ -0,0 +1,22 @@
name: Build and Test
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: maven
- name: Build and Test with Maven
run: mvn --batch-mode --update-snapshots verify
+70
View File
@@ -0,0 +1,70 @@
name: Make Release
on:
workflow_dispatch:
inputs:
version_number:
description: 'Version Number'
required: true
type: string
info:
description: 'Description of this Release'
required: true
type: string
jobs:
build_jar:
name: Build Jar
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: maven
- name: Set Version
uses: datamonsters/replace-action@v2
with:
files: 'pom.xml'
replacements: 'Snapshot=${{ github.event.inputs.version_number }}'
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
- name: Rename jar
run: mv target/*-All.jar JMusicBot-${{ github.event.inputs.version_number }}.jar
- name: Upload jar
uses: actions/upload-artifact@v3
with:
name: jar
path: JMusicBot-${{ github.event.inputs.version_number }}.jar
if-no-files-found: error
create_release:
name: Create Release
runs-on: ubuntu-latest
needs: build_jar
steps:
- name: Download a Build Artifact
uses: actions/download-artifact@v3.0.0
with:
name: jar
path: .
- name: Show Artifacts
run: ls -R
- name: Create Release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.event.inputs.version_number }}
name: Version ${{ github.event.inputs.version_number }}
draft: true
prerelease: false
artifacts: "*.jar"
body: |
${{ github.event.inputs.info }}
---
### Setup
https://jmusicbot.com/setup
https://jmusicbot.com/config
# Download: [JMusicBot-${{ github.event.inputs.version_number }}.jar](https://github.com/jagrosh/MusicBot/releases/download/${{ github.event.inputs.version_number }}/JMusicBot-${{ github.event.inputs.version_number }}.jar)
+3 -1
View File
@@ -4,4 +4,6 @@
/test/
*.json
*.txt
nb*.xml
nb*.xml
dockerfile
.*
+110 -48
View File
@@ -1,60 +1,122 @@
<img align="right" src="https://i.imgur.com/zrE80HY.png" height="200" width="200">
# JMusicBot
# JMusicBot Fork
[![Downloads](https://img.shields.io/github/downloads/jagrosh/MusicBot/total.svg)](https://github.com/jagrosh/MusicBot/releases/latest)
[![Stars](https://img.shields.io/github/stars/jagrosh/MusicBot.svg)](https://github.com/jagrosh/MusicBot/stargazers)
[![Release](https://img.shields.io/github/release/jagrosh/MusicBot.svg)](https://github.com/jagrosh/MusicBot/releases/latest)
[![License](https://img.shields.io/github/license/jagrosh/MusicBot.svg)](https://github.com/jagrosh/MusicBot/blob/master/LICENSE)
[![Discord](https://discordapp.com/api/guilds/147698382092238848/widget.png)](https://discord.gg/0p9LSGoRLu6Pet0k)<br>
[![CircleCI](https://img.shields.io/circleci/project/github/jagrosh/MusicBot/master.svg)](https://circleci.com/gh/jagrosh/MusicBot)
[![AppVeyor](https://ci.appveyor.com/api/projects/status/gdu6nyte5psj6xfk/branch/master?svg=true)](https://ci.appveyor.com/project/jagrosh/musicbot/branch/master)
[![CodeFactor](https://www.codefactor.io/repository/github/jagrosh/musicbot/badge)](https://www.codefactor.io/repository/github/jagrosh/musicbot)
A self-hosted Discord music bot fork based on JMusicBot, updated to keep working on modern Discord and JDA versions.
A cross-platform Discord music bot with a clean interface, and that is easy to set up and run yourself!
This repository keeps the bot practical to run today, while also adding fork-specific features such as status messages and playback analytics logging. Possibly more.
[![Setup](http://i.imgur.com/VvXYp5j.png)](https://jmusicbot.com/setup)
## Demo
Temporary invite: [discord.gg/4cp9CvjwaW](https://discord.gg/4cp9CvjwaW)
Permanent invite: [discord.gg/hK95396uB3](https://discord.gg/hK95396uB3)
## What This Fork Adds
- Make it work again, through:
- JDA 6 support and current dependency updates
- DAVE voice encryption support for current Discord voice requirements
- Optional YouTube account login support
- Optional YouTube PO token and visitor-data support
- New niche features, such as:
- Optional per-channel playback status message
- Optional JDBC/SQLite playback analytics logging
- Basic QoL improvements, such as:
- Docker image and container-friendly deployment path
## Quick Links
- Fork changes and version notes: [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md)
- Setup guide: [docs/setup.md](/git/MusicBot/docs/setup.md)
- Docker usage: [docs/docker.md](/git/MusicBot/docs/docker.md)
## Features
* Easy to run (just make sure Java is installed, and run!)
* Fast loading of songs
* No external keys needed (besides a Discord Bot token)
* Smooth playback
* Server-specific setup for the "DJ" role that can moderate the music
* Clean and beautiful menus
* Supports many sites, including Youtube, Soundcloud, and more
* Supports many online radio/streams
* Supports local files
* Playlist support (both web/youtube, and local)
## Supported sources and formats
JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats):
### Sources
* YouTube
* SoundCloud
* Bandcamp
* Vimeo
* Twitch streams
* Local files
* HTTP URLs
### Formats
* MP3
* FLAC
* WAV
* Matroska/WebM (AAC, Opus or Vorbis codecs)
* MP4/M4A (AAC codec)
* OGG streams (Opus, Vorbis and FLAC codecs)
* AAC streams
* Stream playlists (M3U and PLS)
- Easy self-hosting
- Fast song loading
- Smooth playback
- DJ role and server-specific settings
- Queue management and playlist support
- Support for many sources, streams, and local files
## Example
![Loading Example...](https://i.imgur.com/kVtTKvS.gif)
## Supported Sources And Formats
## Setup
Please see the [Setup Page](https://jmusicbot.com/setup) to run this bot yourself!
JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats).
## Questions/Suggestions/Bug Reports
**Please read the [Issues List](https://github.com/jagrosh/MusicBot/issues) before suggesting a feature**. If you have a question, need troubleshooting help, or want to brainstorm a new feature, please start a [Discussion](https://github.com/jagrosh/MusicBot/discussions). If you'd like to suggest a feature or report a reproducible bug, please open an [Issue](https://github.com/jagrosh/MusicBot/issues) on this repository. If you like this bot, be sure to add a star to the libraries that make this possible: [**JDA**](https://github.com/DV8FromTheWorld/JDA) and [**lavaplayer**](https://github.com/sedmelluq/lavaplayer)!
Sources:
- YouTube
- SoundCloud
- Bandcamp
- Vimeo
- Twitch streams
- Local files
- HTTP URLs
## Editing
This bot (and the source code here) might not be easy to edit for inexperienced programmers. The main purpose of having the source public is to show the capabilities of the libraries, to allow others to understand how the bot works, and to allow those knowledgeable about java, JDA, and Discord bot development to contribute. There are many requirements and dependencies required to edit and compile it, and there will not be support provided for people looking to make changes on their own. Instead, consider making a feature request (see the above section). If you choose to make edits, please do so in accordance with the Apache 2.0 License.
Formats:
- MP3
- FLAC
- WAV
- Matroska/WebM
- MP4/M4A
- OGG streams
- AAC streams
- Stream playlists such as M3U and PLS
## Setup Notes
If you only need the essentials:
1. Create a Discord bot and get its token, see [here](https://jmusicbot.com/getting-a-bot-token/).
2. Enable the `MESSAGE_CONTENT` intent in the Discord Developer Portal [here](https://discord.com/developers/applications) ([Wiki link](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/)).
3. Configure `config.txt`.
4. Run the jar directly or use Docker.
For the detailed version, see [docs/setup.md](/git/MusicBot/docs/setup.md).
## Docker
A Docker image is published here: [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
For compose examples and deployment notes, see [docs/docker.md](/git/MusicBot/docs/docker.md).
## YouTube Login
This fork supports Google account login for YouTube playback workarounds.
Enable:
```conf
youtubeoauth2=true
```
Do not use your main Google account. Use a burner or otherwise disposable account instead.
More details are in [docs/setup.md](/git/MusicBot/docs/setup.md).
If you use PO tokens or visitor data for YouTube, those details are also covered in [docs/setup.md](/git/MusicBot/docs/setup.md).
## DAVE / Voice Encryption
Discord currently requires DAVE for voice on modern API versions. This fork configures `libdave-jvm` during startup and bundles native binaries for common Linux, Windows, and macOS targets.
If you deploy on an unsupported architecture, add the matching native dependency in [pom.xml](/git/MusicBot/pom.xml). You probably need to build the library from source for your architecture, so have fun with that.
## Upstream And Related Work
This fork incorporates ideas and patches related to:
- [PR-1670](https://github.com/jagrosh/MusicBot/pull/1670)
- [PR-1703](https://github.com/jagrosh/MusicBot/pull/1703)
- [PR-1772](https://github.com/jagrosh/MusicBot/pull/1772)
If this fork does not fit your needs, another fork you may want to inspect is [SeVile/MusicBot](https://github.com/SeVile/MusicBot).
## Questions And Contributions
If you run into issues, please include:
- the version you are running
- whether you are using Docker or the jar directly
- relevant config changes
- relevant logs
Fork-specific version notes live in [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md), which should help with troubleshooting and migration as the fork evolves.
+95
View File
@@ -0,0 +1,95 @@
# Docker Guide
This fork supports running the bot in Docker, which is usually the easiest deployment path for a server.
## Image
Published image:
- [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
Container build sources:
- [github.com/chrisb09/jmb-container](https://github.com/chrisb09/jmb-container)
## Minimal Compose Example
```yaml
version: "2.2"
services:
jmusicbot:
image: chrisb09/jmusicbot:latest
container_name: jmusicbot
restart: unless-stopped
volumes:
- ./config:/jmb/config
```
Replace `./config` with the host path you actually want to use.
## Config Files
Your persistent bot config should live in the mounted config directory. That lets you redeploy containers without losing your bot settings.
Typical things to configure there:
- bot token
- owner ID
- prefix
- YouTube login toggle
- YouTube PO token and visitor-data values if needed
- status message toggle
- analytics logging settings
## Deploy Flow
A typical update flow is:
1. Pull or rebuild the image.
2. Restart the container.
3. Check logs for startup errors.
Useful commands:
```bash
docker-compose up -d
docker-compose logs -f jmusicbot
```
## Alpine / musl Note
This fork bundles native support for Alpine-compatible musl environments as part of its DAVE setup, so Alpine-based container deployments are supported.
## SQLite Logging In Docker
If you enable analytics logging with SQLite:
```conf
dblog.enabled = true
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.
Relevant config values include:
```conf
youtubeoauth2 = true
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
## Recommendation
For production-style use, Docker is the cleanest path if you want:
- reproducible runtime behavior
- simple upgrades
- easier separation between bot code and persistent config
+57
View File
@@ -0,0 +1,57 @@
# Fork Changelog
This file tracks fork-specific changes that are useful to operators and contributors. It is not meant to replace commit history, but to summarize what changed at a higher level.
## 0.6.3
- Added JDBC-backed playback analytics logging.
- Added SQLite as the default logging backend.
- Logged queue, playback, listener, search, skip, and command activity.
- Added `dblog.*` config settings to enable and configure logging.
Config:
```conf
dblog.enabled = true
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
dblog.user = ""
dblog.password = ""
```
By default, logging is disabled.
## 0.6.2
- Fixed several status-message race conditions and repost issues.
- Added debug logging around status-message creation, editing, deletion, and repost flow.
- Added a config toggle to disable the status-message feature entirely.
Config:
```conf
statusmessages = true
```
Set it to `false` to disable the feature.
## 0.6.1
- Bumped the fork version after the initial status-message work had landed.
## 0.6.0
- Updated the bot for modern JDA and Discord requirements.
- Added DAVE voice support required for current Discord voice playback.
- Continued dependency refreshes for lavaplayer and related components.
- Carried forward work related to YouTube login, PO token handling, and buildability on the newer stack.
Imported work included in this fork:
- PR-1670, support for logging into a YouTube account, is included from `0.4.5` onward.
- PR-1703, changes to allow building the project from source more easily, is included from `0.4.5` onward.
- `jagrosh#1772`, YouTube PO token and IP rotation support, is included from `0.4.7` onward.
## Notes
- This fork may move faster than upstream in areas related to Discord API compatibility and deployment practicality.
- When reporting issues, mention the exact fork version because behavior around playback, status messages, and logging has changed across recent releases.
+178
View File
@@ -0,0 +1,178 @@
# Setup Guide
This guide covers the normal non-Docker setup path and the main configuration points that matter on this fork.
## Prerequisites
- Java 11 or newer
- A Discord bot token
- A Discord application with the bot invited to your server
## Discord Setup
1. Create a bot in the [Discord Developer Portal](https://discord.com/developers/applications).
2. Copy the bot token.
3. Enable the `MESSAGE_CONTENT` intent.
This intent is required for command-based bots like this one.
## Privileged Intent Notes
This fork moved from JDA 4 to JDA 6, so `MESSAGE_CONTENT` is now an important operational requirement for the classic text-command flow.
Keep in mind:
- If the bot joins more than 100 servers, this intent must be enabled in the [Discord Developer Portal](https://discord.com/developers/applications).
- `MESSAGE_CONTENT` is a privileged intent.
- Discord verification requirements become relevant as the bot grows.
- The related portal controls historically only become visible once the application is already in a fairly large number of servers.
For the broader Discord/JDA behavior, see the [JDA gateway intents documentation](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/).
## First Configuration
The bot reads from `config.txt` using the values defined in [reference.conf](/git/MusicBot/src/main/resources/reference.conf).
The minimum settings you should care about first are:
```conf
token = "YOUR_BOT_TOKEN"
owner = 123456789012345678
prefix = "@mention"
```
Useful fork-specific options:
```conf
youtubeoauth2 = true
statusmessages = true
dblog.enabled = false
```
You may also need these for YouTube PO token support:
```conf
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
## Running The Bot
Build:
```bash
mvn clean package
```
Run:
```bash
java -jar target/JMusicBot-*-All.jar
```
This fork includes changes to keep source builds practical on the newer stack as well.
## YouTube Login
If YouTube playback is unreliable, enable:
```conf
youtubeoauth2 = true
```
After startup, the bot prints instructions and sends the owner a DM with the authorization flow.
Important:
- Do not use your main Google account.
- Use a burner or otherwise disposable account.
If you are starting from a clean config, this option should already exist with a default of `false`.
## YouTube PO Tokens And Visitor Data
This fork also supports explicit YouTube PO token and visitor-data configuration.
Config:
```conf
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
You can generate these values with:
- [iv-org/youtube-trusted-session-generator](https://github.com/iv-org/youtube-trusted-session-generator)
This is useful when normal YouTube playback is not enough and you need a more explicit trusted-session style setup.
## DAVE / Voice Support
This fork includes DAVE support for current Discord voice requirements. In normal supported environments, nothing extra is required beyond using the provided shaded jar.
If you run on an unsupported architecture, add the correct native `libdave-jvm` dependency in [pom.xml](/git/MusicBot/pom.xml).
Bundled native targets include:
- Linux glibc x86_64
- Linux musl x86_64
- Windows x86_64
- macOS
## Status Messages
The bot can maintain a single playback status message in the active text channel.
Config:
```conf
statusmessages = true
```
Disable it with:
```conf
statusmessages = false
```
## Analytics Logging
Playback analytics logging is off by default.
Enable SQLite logging:
```conf
dblog.enabled = true
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.
## Related Fork Context
This fork incorporates or builds on work related to:
- [PR-1670](https://github.com/jagrosh/MusicBot/pull/1670) for YouTube account login support
- [PR-1703](https://github.com/jagrosh/MusicBot/pull/1703) for source builds
- [PR-1772](https://github.com/jagrosh/MusicBot/pull/1772) for YouTube PO token and IP rotation work
If this fork does not fit your setup, another fork you may want to inspect is [SeVile/MusicBot](https://github.com/SeVile/MusicBot).
+203 -41
View File
@@ -3,10 +3,15 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.jagrosh</groupId>
<artifactId>JMusicBot</artifactId>
<version>Snapshot</version>
<version>0.6.6</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>m2-chew</id>
<name>Maven Chewtral</name>
<url>https://m2.chew.pro/releases</url>
</repository>
<repository>
<id>dv8tion</id>
<name>m2-dv8tion</name>
@@ -14,59 +19,176 @@
</repository>
<repository>
<id>central</id>
<name>bintray</name>
<url>https://jcenter.bintray.com</url>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
<repository>
<id>m2.duncte123.dev</id>
<name>m2-duncte123</name>
<url>https://m2.duncte123.dev/releases</url>
</repository>
<repository>
<id>arbjergDev</id>
<name>Lavalink Repository</name>
<url>https://maven.lavalink.dev/releases</url>
</repository>
<repository>
<id>arbjergDevSnapshots</id>
<name>Lavalink Repository</name>
<url>https://maven.lavalink.dev/snapshots</url>
</repository>
</repositories>
<dependencies>
<!-- Discord Dependencies -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>4.3.0_324</version>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>6.3.1</version>
</dependency>
<!-- using a fork of this to fix some issues faster -->
<!-- dependency>
<groupId>com.sedmelluq</groupId>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>adapter-jda</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>impl-jni</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-linux-x86-64</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-linux-musl-x86-64</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-win-x86-64</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-darwin</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-command</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-examples</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<!-- <dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-command</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>2.2.1</version>
</dependency> -->
<!--
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-command</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>2.2.1</version>
</dependency>
-->
<!-- Music Dependencies -->
<dependency>
<groupId>dev.arbjerg</groupId>
<artifactId>lavaplayer</artifactId>
<version>1.3.78</version>
</dependency -->
<dependency>
<groupId>com.github.jagrosh</groupId>
<artifactId>lavaplayer</artifactId>
<version>jmusicbot-SNAPSHOT</version>
</dependency>
<!-- this is needed, but isn't actually hosted anywhere anymore... uh -->
<!--dependency>
<groupId>com.sedmelluq</groupId>
<artifactId>lavaplayer-natives-extra</artifactId>
<version>1.3.13</version>
</dependency-->
<dependency>
<groupId>com.jagrosh</groupId>
<artifactId>jda-utilities</artifactId>
<version>3.0.5</version>
<type>pom</type>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>dev.arbjerg</groupId>
<artifactId>lavaplayer-ext-youtube-rotator</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>dev.lavalink.youtube</groupId>
<artifactId>common</artifactId>
<version>1.18.1</version>
</dependency>
<!-- jitpack for now, we need to version this correctly later -->
<dependency>
<groupId>com.github.jagrosh</groupId>
<artifactId>JLyrics</artifactId>
<version>-SNAPSHOT</version>
<version>master</version>
</dependency>
<!--dependency>
<groupId>com.jagrosh</groupId>
<artifactId>JLyrics</artifactId>
<version>0.6</version>
</dependency-->
<dependency>
<groupId>com.dunctebot</groupId>
<artifactId>sourcemanagers</artifactId>
<version>1.9.0</version>
</dependency>
<!-- Misc Internal Dependencies -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.1.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
@@ -78,6 +200,35 @@
<artifactId>jsoup</artifactId>
<version>1.14.2</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.27.0</version>
<!-- https://github.com/SeleniumHQ/selenium/issues/13791 -->
<exclusions>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-devtools-v85</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-devtools-v129</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-devtools-v130</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.2</version>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@@ -91,8 +242,18 @@
<scope>test</scope>
</dependency>
</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>
@@ -133,10 +294,11 @@
</plugin>
</plugins>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<jda.chewtils.version>2.2.1</jda.chewtils.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
+2 -2
View File
@@ -9,9 +9,9 @@ DOWNLOAD=true
LOOP=true
download() {
if [ $DOWNLOAD == true ]; then
if [ $DOWNLOAD = true ]; then
URL=$(curl -s https://api.github.com/repos/jagrosh/MusicBot/releases/latest \
| grep -i browser_download_url.*\.jar \
| grep -i "browser_download_url.*\.jar" \
| sed 's/.*\(http.*\)"/\1/')
FILENAME=$(echo $URL | sed 's/.*\/\([^\/]*\)/\1/')
if [ -f $FILENAME ]; then
@@ -0,0 +1 @@
jda-chewtils.version=${jda.chewtils.version}
+50 -2
View File
@@ -22,10 +22,14 @@ import com.jagrosh.jmusicbot.audio.AloneInVoiceHandler;
import com.jagrosh.jmusicbot.audio.AudioHandler;
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;
import java.util.Objects;
import com.jagrosh.jmusicbot.utils.YoutubeOauth2TokenHandler;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
@@ -43,7 +47,11 @@ public class Bot
private final PlayerManager players;
private final PlaylistLoader playlists;
private final NowplayingHandler nowplaying;
private final StatusMessageHandler statusMessageHandler;
private final AloneInVoiceHandler aloneInVoiceHandler;
private final DataLogService dataLogService;
private final StatsReportScheduler statsReportScheduler;
private final YoutubeOauth2TokenHandler youTubeOauth2TokenHandler;
private boolean shuttingDown = false;
private JDA jda;
@@ -56,12 +64,31 @@ public class Bot
this.settings = settings;
this.playlists = new PlaylistLoader(config);
this.threadpool = Executors.newSingleThreadScheduledExecutor();
this.players = new PlayerManager(this);
this.youTubeOauth2TokenHandler = new YoutubeOauth2TokenHandler();
this.youTubeOauth2TokenHandler.init();
this.players = new PlayerManager(this, config);
this.players.init();
this.nowplaying = new NowplayingHandler(this);
this.nowplaying.init();
this.statusMessageHandler = new StatusMessageHandler(this);
this.statusMessageHandler.init();
this.aloneInVoiceHandler = new AloneInVoiceHandler(this);
this.aloneInVoiceHandler.init();
DataLogService dls = null;
if(config.getDataLogEnabled())
{
try
{
dls = new DataLogService(this, config.getDataLogJdbcUrl(), config.getDataLogUser(), config.getDataLogPassword());
}
catch(Exception ex)
{
dls = null;
}
}
this.dataLogService = dls;
this.statsReportScheduler = new StatsReportScheduler(this);
this.statsReportScheduler.init();
}
public BotConfig getConfig()
@@ -99,10 +126,30 @@ public class Bot
return nowplaying;
}
public StatusMessageHandler getStatusMessageHandler()
{
return statusMessageHandler;
}
public AloneInVoiceHandler getAloneInVoiceHandler()
{
return aloneInVoiceHandler;
}
public DataLogService getDataLogService()
{
return dataLogService;
}
public StatsReportScheduler getStatsReportScheduler()
{
return statsReportScheduler;
}
public YoutubeOauth2TokenHandler getYouTubeOauth2Handler()
{
return youTubeOauth2TokenHandler;
}
public JDA getJDA()
{
@@ -139,13 +186,14 @@ public class Bot
{
ah.stopAndClear();
ah.getPlayer().destroy();
nowplaying.updateTopic(g.getIdLong(), ah, true);
}
});
jda.shutdown();
}
if(gui!=null)
gui.dispose();
if(dataLogService != null)
dataLogService.shutdown();
System.exit(0);
}
@@ -16,14 +16,18 @@
package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.entities.Prompt;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.jagrosh.jmusicbot.utils.YouTubeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
import com.typesafe.config.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
@@ -40,10 +44,18 @@ public class BotConfig
private final static String END_TOKEN = "/// END OF JMUSICBOT CONFIG ///";
private Path path = null;
private String token, prefix, altprefix, helpWord, playlistsFolder,
successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji;
private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots;
private String token, prefix, altprefix, helpWord, playlistsFolder, logLevel,
successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji,
chromePath, chromeDriverPath, evalEngine;
private YouTubeUtil.RoutingPlanner ytRoutingPlanner;
private List<IpBlock> ytIpBlocks;
private boolean youtubeOauth2, stayInChannel, songInGame, npImages, updatealerts, chromeHeadless, useEval, dbots;
private boolean statusMessages;
private boolean dataLogEnabled;
private String dataLogJdbcUrl, dataLogUser, dataLogPassword;
private long owner, maxSeconds, aloneTimeUntilStop;
private int maxYTPlaylistPages;
private double skipratio;
private OnlineStatus status;
private Activity game;
private Config aliases, transforms;
@@ -63,13 +75,7 @@ public class BotConfig
try
{
// get the path to the config, default config.txt
path = OtherUtil.getPath(System.getProperty("config.file", System.getProperty("config", "config.txt")));
if(path.toFile().exists())
{
if(System.getProperty("config.file") == null)
System.setProperty("config.file", System.getProperty("config", path.toAbsolutePath().toString()));
ConfigFactory.invalidateCaches();
}
path = getConfigPath();
// load in the config file, plus the default values
//Config config = ConfigFactory.parseFile(path.toFile()).withFallback(ConfigFactory.load());
@@ -88,17 +94,32 @@ public class BotConfig
searchingEmoji = config.getString("searching");
game = OtherUtil.parseGame(config.getString("game"));
status = OtherUtil.parseStatus(config.getString("status"));
youtubeOauth2 = config.getBoolean("youtubeoauth2");
stayInChannel = config.getBoolean("stayinchannel");
songInGame = config.getBoolean("songinstatus");
statusMessages = config.getBoolean("statusmessages");
npImages = config.getBoolean("npimages");
updatealerts = config.getBoolean("updatealerts");
logLevel = config.getString("loglevel");
useEval = config.getBoolean("eval");
evalEngine = config.getString("evalengine");
maxSeconds = config.getLong("maxtime");
maxYTPlaylistPages = config.getInt("maxytplaylistpages");
aloneTimeUntilStop = config.getLong("alonetimeuntilstop");
playlistsFolder = config.getString("playlistsfolder");
aliases = config.getConfig("aliases");
chromePath = config.getString("chromepath");
chromeDriverPath = config.getString("chromedriverpath");
chromeHeadless = config.getBoolean("chromeheadless");
ytRoutingPlanner = config.getEnum(YouTubeUtil.RoutingPlanner.class, "ytroutingplanner");
ytIpBlocks = config.getStringList("ytipblocks").stream().map(YouTubeUtil::parseIpBlock).collect(Collectors.toList());
transforms = config.getConfig("transforms");
skipratio = config.getDouble("skipratio");
dbots = owner == 113156185389092864L;
dataLogEnabled = config.getBoolean("dblog.enabled");
dataLogJdbcUrl = config.getString("dblog.jdbc_url");
dataLogUser = config.getString("dblog.user");
dataLogPassword = config.getString("dblog.password");
// we may need to write a new config file
boolean write = false;
@@ -161,19 +182,9 @@ public class BotConfig
private void writeToFile()
{
String original = OtherUtil.loadResource(this, "/reference.conf");
byte[] bytes;
if(original==null)
{
bytes = ("token = "+token+"\r\nowner = "+owner).getBytes();
}
else
{
bytes = original.substring(original.indexOf(START_TOKEN)+START_TOKEN.length(), original.indexOf(END_TOKEN))
.replace("BOT_TOKEN_HERE", token)
byte[] bytes = loadDefaultConfig().replace("BOT_TOKEN_HERE", token)
.replace("0 // OWNER ID", Long.toString(owner))
.trim().getBytes();
}
try
{
Files.write(path, bytes);
@@ -186,6 +197,43 @@ public class BotConfig
}
}
private static String loadDefaultConfig()
{
String original = OtherUtil.loadResource(new JMusicBot(), "/reference.conf");
return original==null
? "token = BOT_TOKEN_HERE\r\nowner = 0 // OWNER ID"
: original.substring(original.indexOf(START_TOKEN)+START_TOKEN.length(), original.indexOf(END_TOKEN)).trim();
}
private static Path getConfigPath()
{
Path path = OtherUtil.getPath(System.getProperty("config.file", System.getProperty("config", "config.txt")));
if(path.toFile().exists())
{
if(System.getProperty("config.file") == null)
System.setProperty("config.file", System.getProperty("config", path.toAbsolutePath().toString()));
ConfigFactory.invalidateCaches();
}
return path;
}
public static void writeDefaultConfig()
{
Prompt prompt = new Prompt(null, null, true, true);
prompt.alert(Prompt.Level.INFO, "JMusicBot Config", "Generating default config file");
Path path = BotConfig.getConfigPath();
byte[] bytes = BotConfig.loadDefaultConfig().getBytes();
try
{
prompt.alert(Prompt.Level.INFO, "JMusicBot Config", "Writing default config file to " + path.toAbsolutePath().toString());
Files.write(path, bytes);
}
catch(Exception ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot Config", "An error occurred writing the default config file: " + ex.getMessage());
}
}
public boolean isValid()
{
return valid;
@@ -211,10 +259,35 @@ public class BotConfig
return token;
}
public double getSkipRatio()
{
return skipratio;
}
public long getOwnerId()
{
return owner;
}
public boolean getDataLogEnabled()
{
return dataLogEnabled;
}
public String getDataLogJdbcUrl()
{
return dataLogJdbcUrl;
}
public String getDataLogUser()
{
return dataLogUser;
}
public String getDataLogPassword()
{
return dataLogPassword;
}
public String getSuccess()
{
@@ -246,6 +319,11 @@ public class BotConfig
return game;
}
public boolean isGameNone()
{
return game != null && game.getName().equalsIgnoreCase("none");
}
public OnlineStatus getStatus()
{
return status;
@@ -256,6 +334,11 @@ public class BotConfig
return helpWord;
}
public boolean useYoutubeOauth2()
{
return youtubeOauth2;
}
public boolean getStay()
{
return stayInChannel;
@@ -265,6 +348,11 @@ public class BotConfig
{
return songInGame;
}
public boolean useStatusMessages()
{
return statusMessages;
}
public String getPlaylistsFolder()
{
@@ -280,12 +368,47 @@ public class BotConfig
{
return updatealerts;
}
public String getLogLevel()
{
return logLevel;
}
public String getChromePath()
{
return chromePath.equals("AUTO") ? null : chromePath;
}
public String getChromeDriverPath()
{
return chromeDriverPath.equals("AUTO") ? null : chromeDriverPath;
}
public boolean getChromeHeadless()
{
return chromeHeadless;
}
public YouTubeUtil.RoutingPlanner getYTRoutingPlanner()
{
return ytRoutingPlanner;
}
public List<IpBlock> getYTIpBlocks()
{
return ytIpBlocks;
}
public boolean useEval()
{
return useEval;
}
public String getEvalEngine()
{
return evalEngine;
}
public boolean useNPImages()
{
return npImages;
@@ -296,9 +419,14 @@ public class BotConfig
return maxSeconds;
}
public int getMaxYTPlaylistPages()
{
return maxYTPlaylistPages;
}
public String getMaxTime()
{
return FormatUtil.formatTime(maxSeconds * 1000);
return TimeUtil.formatTime(maxSeconds * 1000);
}
public long getAloneTimeUntilStop()
@@ -15,9 +15,11 @@
*/
package com.jagrosh.jmusicbot;
import com.jagrosh.jdautilities.command.CommandClient;
import com.jagrosh.jdautilities.command.CommandClientBuilder;
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
import com.jagrosh.jdautilities.examples.command.*;
import com.jagrosh.jmusicbot.commands.general.LoggedAboutCmd;
import com.jagrosh.jmusicbot.commands.general.LoggedPingCmd;
import com.jagrosh.jmusicbot.commands.admin.*;
import com.jagrosh.jmusicbot.commands.dj.*;
import com.jagrosh.jmusicbot.commands.general.*;
@@ -27,15 +29,22 @@ import com.jagrosh.jmusicbot.entities.Prompt;
import com.jagrosh.jmusicbot.gui.GUI;
import com.jagrosh.jmusicbot.settings.SettingsManager;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import moe.kyokobot.libdave.NativeDaveFactory;
import moe.kyokobot.libdave.jda.LDJDADaveSessionFactory;
import java.awt.Color;
import java.util.Arrays;
import javax.security.auth.login.LoginException;
import net.dv8tion.jda.api.*;
import net.dv8tion.jda.api.audio.AudioModuleConfig;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import static com.jagrosh.jmusicbot.utils.YouTubeUtil.GetPoTokenAndVisitorData;
/**
*
@@ -43,44 +52,151 @@ import org.slf4j.LoggerFactory;
*/
public class JMusicBot
{
public final static String PLAY_EMOJI = "\u25B6"; // ▶
public final static String PAUSE_EMOJI = "\u23F8"; // ⏸
public final static String STOP_EMOJI = "\u23F9"; // ⏹
public final static Permission[] RECOMMENDED_PERMS = {Permission.MESSAGE_READ, Permission.MESSAGE_WRITE, Permission.MESSAGE_HISTORY, Permission.MESSAGE_ADD_REACTION,
public final static Logger LOG = LoggerFactory.getLogger(JMusicBot.class);
public final static Permission[] RECOMMENDED_PERMS = {Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND, Permission.MESSAGE_HISTORY, Permission.MESSAGE_ADD_REACTION,
Permission.MESSAGE_EMBED_LINKS, Permission.MESSAGE_ATTACH_FILES, Permission.MESSAGE_MANAGE, Permission.MESSAGE_EXT_EMOJI,
Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT, Permission.VOICE_SPEAK, Permission.NICKNAME_CHANGE};
public final static GatewayIntent[] INTENTS = {GatewayIntent.DIRECT_MESSAGES, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_VOICE_STATES};
Permission.VOICE_CONNECT, Permission.VOICE_SPEAK, Permission.NICKNAME_CHANGE};
public final static GatewayIntent[] INTENTS = {GatewayIntent.DIRECT_MESSAGES, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_VOICE_STATES, GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_EXPRESSIONS, GatewayIntent.SCHEDULED_EVENTS};
/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
// startup log
Logger log = LoggerFactory.getLogger("Startup");
if(args.length > 0)
switch(args[0].toLowerCase())
{
case "generate-config":
BotConfig.writeDefaultConfig();
return;
default:
}
startBot();
}
private static void startBot()
{
// create prompt to handle startup
Prompt prompt = new Prompt("JMusicBot", "Switching to nogui mode. You can manually start in nogui mode by including the -Dnogui=true flag.");
Prompt prompt = new Prompt("JMusicBot");
// get and check latest version
String version = OtherUtil.checkVersion(prompt);
// check for valid java version
if(!System.getProperty("java.vm.name").contains("64"))
prompt.alert(Prompt.Level.WARNING, "Java Version", "It appears that you may not be using a supported Java version. Please use 64-bit java.");
// startup checks
OtherUtil.checkVersion(prompt);
OtherUtil.checkJavaVersion(prompt);
// load config
BotConfig config = new BotConfig(prompt);
config.load();
if(!config.isValid())
return;
LOG.info("Loaded config from " + config.getConfigLocation());
// set log level from config
((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(
Level.toLevel(config.getLogLevel(), Level.INFO));
// set up the listener
EventWaiter waiter = new EventWaiter();
SettingsManager settings = new SettingsManager();
Bot bot = new Bot(waiter, config, settings);
CommandClient client = createCommandClient(config, settings, bot);
AboutCommand aboutCommand = new AboutCommand(Color.BLUE.brighter(),
"a music bot that is [easy to host yourself!](https://github.com/jagrosh/MusicBot) (v"+version+")",
if(!prompt.isNoGUI())
{
try
{
GUI gui = new GUI(bot);
bot.setGUI(gui);
gui.init();
LOG.info("Loaded config from " + config.getConfigLocation());
}
catch(Exception e)
{
LOG.error("Could not start GUI. If you are "
+ "running on a server or in a location where you cannot display a "
+ "window, please run in nogui mode using the -Dnogui=true flag.");
}
}
// attempt to log in and start
try
{
JDA jda = JDABuilder.create(config.getToken(), Arrays.asList(INTENTS))
.enableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE)
.disableCache(CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS, CacheFlag.ONLINE_STATUS)
.setActivity(config.isGameNone() ? null : Activity.playing("loading..."))
.setStatus(config.getStatus()==OnlineStatus.INVISIBLE || config.getStatus()==OnlineStatus.OFFLINE
? OnlineStatus.INVISIBLE : OnlineStatus.DO_NOT_DISTURB)
.setAudioModuleConfig(createAudioModuleConfig())
.addEventListeners(client, waiter, new Listener(bot))
.setBulkDeleteSplittingEnabled(true)
.build();
bot.setJDA(jda);
// check if something about the current startup is not supported
String unsupportedReason = OtherUtil.getUnsupportedBotReason(jda);
if (unsupportedReason != null)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "JMusicBot cannot be run on this Discord bot: " + unsupportedReason);
try{ Thread.sleep(5000);}catch(InterruptedException ignored){} // this is awful but until we have a better way...
jda.shutdown();
System.exit(1);
}
// other check that will just be a warning now but may be required in the future
// check if the user has changed the prefix and provide info about the
// message content intent
if(!"@mention".equals(config.getPrefix()))
{
LOG.info("JMusicBot", "You currently have a custom prefix set. "
+ "If your prefix is not working, make sure that the 'MESSAGE CONTENT INTENT' is Enabled "
+ "on https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot");
}
}
catch(IllegalStateException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex.getMessage());
LOG.error("Failed to initialize DAVE protocol support", ex);
System.exit(1);
}
catch(IllegalArgumentException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "Some aspect of the configuration is "
+ "invalid: " + ex + "\nConfig Location: " + config.getConfigLocation());
System.exit(1);
}
catch(ErrorResponseException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex + "\nInvalid reponse returned when "
+ "attempting to connect, please make sure you're connected to the internet");
System.exit(1);
}
GetPoTokenAndVisitorData(config.getChromePath(), config.getChromeDriverPath(), config.getChromeHeadless());
}
private static AudioModuleConfig createAudioModuleConfig()
{
try
{
NativeDaveFactory.ensureAvailable();
LOG.info("Configured DAVE protocol support using libdave-jvm.");
return new AudioModuleConfig()
.withDaveSessionFactory(new LDJDADaveSessionFactory(new NativeDaveFactory()));
}
catch(Throwable ex)
{
throw new IllegalStateException("Unable to initialize DAVE protocol support via libdave-jvm. "
+ "Verify native dependencies are present for this platform (Linux glibc/musl x86_64, Windows x86_64, or macOS).", ex);
}
}
private static CommandClient createCommandClient(BotConfig config, SettingsManager settings, Bot bot)
{
// instantiate about command
LoggedAboutCmd aboutCommand = new LoggedAboutCmd(bot, Color.BLUE.brighter(),
"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);
@@ -96,8 +212,9 @@ public class JMusicBot
.setLinkedCacheSize(200)
.setGuildSettingsManager(settings)
.addCommands(aboutCommand,
new PingCommand(),
new LoggedPingCmd(bot),
new SettingsCmd(bot),
new StatsCmd(bot),
new LyricsCmd(bot),
new NowplayingCmd(bot),
@@ -107,6 +224,7 @@ public class JMusicBot
new RemoveCmd(bot),
new SearchCmd(bot),
new SCSearchCmd(bot),
new SeekCmd(bot),
new ShuffleCmd(bot),
new SkipCmd(bot),
@@ -121,10 +239,12 @@ public class JMusicBot
new VolumeCmd(bot),
new PrefixCmd(bot),
new QueueTypeCmd(bot),
new SetdjCmd(bot),
new SkipratioCmd(bot),
new SettcCmd(bot),
new SetvcCmd(bot),
new AutoplaylistCmd(bot),
new DebugCmd(bot),
new PlaylistCmd(bot),
@@ -134,65 +254,23 @@ public class JMusicBot
new SetstatusCmd(bot),
new ShutdownCmd(bot)
);
// enable eval if applicable
if(config.useEval())
cb.addCommand(new EvalCmd(bot));
boolean nogame = false;
if(config.getStatus()!=OnlineStatus.UNKNOWN)
// set status if set in config
if(config.getStatus() != OnlineStatus.UNKNOWN)
cb.setStatus(config.getStatus());
if(config.getGame()==null)
// set game
if(config.getGame() == null)
cb.useDefaultGame();
else if(config.getGame().getName().equalsIgnoreCase("none"))
{
else if(config.isGameNone())
cb.setActivity(null);
nogame = true;
}
else
cb.setActivity(config.getGame());
if(!prompt.isNoGUI())
{
try
{
GUI gui = new GUI(bot);
bot.setGUI(gui);
gui.init();
}
catch(Exception e)
{
log.error("Could not start GUI. If you are "
+ "running on a server or in a location where you cannot display a "
+ "window, please run in nogui mode using the -Dnogui=true flag.");
}
}
log.info("Loaded config from " + config.getConfigLocation());
// attempt to log in and start
try
{
JDA jda = JDABuilder.create(config.getToken(), Arrays.asList(INTENTS))
.enableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE)
.disableCache(CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS, CacheFlag.EMOTE, CacheFlag.ONLINE_STATUS)
.setActivity(nogame ? null : Activity.playing("loading..."))
.setStatus(config.getStatus()==OnlineStatus.INVISIBLE || config.getStatus()==OnlineStatus.OFFLINE
? OnlineStatus.INVISIBLE : OnlineStatus.DO_NOT_DISTURB)
.addEventListeners(cb.build(), waiter, new Listener(bot))
.setBulkDeleteSplittingEnabled(true)
.build();
bot.setJDA(jda);
}
catch (LoginException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex + "\nPlease make sure you are "
+ "editing the correct config.txt file, and that you have used the "
+ "correct token (not the 'secret'!)\nConfig Location: " + config.getConfigLocation());
System.exit(1);
}
catch(IllegalArgumentException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "Some aspect of the configuration is "
+ "invalid: " + ex + "\nConfig Location: " + config.getConfigLocation());
System.exit(1);
}
return cb.build();
}
}
@@ -17,15 +17,18 @@ package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import java.util.concurrent.TimeUnit;
import com.jagrosh.jmusicbot.utils.YoutubeOauth2TokenHandler;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.events.ReadyEvent;
import net.dv8tion.jda.api.events.ShutdownEvent;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.events.session.ShutdownEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@@ -82,21 +85,48 @@ public class Listener extends ListenerAdapter
owner.openPrivateChannel().queue(pc -> pc.sendMessage(msg).queue());
}
}
catch(Exception ex) {} // ignored
catch(Exception ignored) {} // ignored
}, 0, 24, TimeUnit.HOURS);
}
if (bot.getConfig().useYoutubeOauth2())
{
YoutubeOauth2TokenHandler.Data data = bot.getYouTubeOauth2Handler().getData();
if (data != null)
{
PrivateChannel channel = bot.getJDA().openPrivateChannelById(bot.getConfig().getOwnerId()).complete();
channel
.sendMessage(
"# DO NOT AUTHORISE THIS WITH YOUR MAIN GOOGLE ACCOUNT!!!\n"
+ "## Create or use an alternative/burner Google account!\n"
+ "To give JMusicBot access to your Google account, go to "
+ data.getAuthorisationUrl()
+ " and enter the code **" + data.getCode() + "**")
.queue();
}
}
}
@Override
public void onGuildMessageDelete(GuildMessageDeleteEvent event)
public void onMessageDelete(MessageDeleteEvent event)
{
bot.getNowplayingHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong());
if(event.isFromGuild()) {
bot.getNowplayingHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong());
bot.getStatusMessageHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong());
}
}
@Override
public void onMessageReceived(MessageReceivedEvent event)
{
bot.getStatusMessageHandler().onMessageReceived(event);
}
@Override
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event)
{
bot.getAloneInVoiceHandler().onVoiceUpdate(event);
if(bot.getDataLogService() != null)
bot.getDataLogService().onVoiceUpdate(event);
}
@Override
@@ -15,11 +15,14 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.JMusicBot;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import com.jagrosh.jmusicbot.queue.AbstractQueue;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
@@ -27,18 +30,19 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.jagrosh.jmusicbot.queue.FairQueue;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack;
import java.nio.ByteBuffer;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
@@ -46,7 +50,12 @@ import net.dv8tion.jda.api.entities.User;
*/
public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
{
private final FairQueue<QueuedTrack> queue = new FairQueue<>();
public final static String PLAY_EMOJI = "\u25B6"; // ▶
public final static String PAUSE_EMOJI = "\u23F8"; // ⏸
public final static String STOP_EMOJI = "\u23F9"; // ⏹
private final static Logger LOGGER = LoggerFactory.getLogger(AudioHandler.class);
private final List<AudioTrack> defaultQueue = new LinkedList<>();
private final Set<String> votes = new HashSet<>();
@@ -55,12 +64,22 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
private final long guildId;
private AudioFrame lastFrame;
private AbstractQueue<QueuedTrack> queue;
private volatile String playCountTrackIdentifier;
private volatile long currentTrackServerPlayCount = -1L;
protected AudioHandler(PlayerManager manager, Guild guild, AudioPlayer player)
{
this.manager = manager;
this.audioPlayer = player;
this.guildId = guild.getIdLong();
this.setQueueType(manager.getBot().getSettingsManager().getSettings(guildId).getQueueType());
}
public void setQueueType(QueueType type)
{
queue = type.createInstance(queue);
}
public int addTrackToFront(QueuedTrack qtrack)
@@ -88,7 +107,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
return queue.add(qtrack);
}
public FairQueue<QueuedTrack> getQueue()
public AbstractQueue<QueuedTrack> getQueue()
{
return queue;
}
@@ -103,7 +122,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
public boolean isMusicPlaying(JDA jda)
{
return guild(jda).getSelfMember().getVoiceState().inVoiceChannel() && audioPlayer.getPlayingTrack()!=null;
return guild(jda).getSelfMember().getVoiceState().inAudioChannel() && audioPlayer.getPlayingTrack()!=null;
}
public Set<String> getVotes()
@@ -140,10 +159,13 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
return false;
pl.loadTracks(manager, (at) ->
{
at.setUserData(RequestMetadata.fromDefaultPlaylist(settings.getDefaultPlaylist()));
if(audioPlayer.getPlayingTrack()==null)
audioPlayer.playTrack(at);
else
defaultQueue.add(at);
if(manager.getBot().getDataLogService() != null)
manager.getBot().getDataLogService().logQueueAdd(guild(manager.getBot().getJDA()), null, at, "DEFAULT_PLAYLIST", null, null, settings.getDefaultPlaylist());
}, () ->
{
if(pl.getTracks().isEmpty() && !manager.getBot().getConfig().getStay())
@@ -156,6 +178,8 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
@Override
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason)
{
if(manager.getBot().getDataLogService() != null)
manager.getBot().getDataLogService().logPlayEnd(guild(manager.getBot().getJDA()), endReason.name());
RepeatMode repeatMode = manager.getBot().getSettingsManager().getSettings(guildId).getRepeatMode();
// if the track ended normally, and we're in repeat mode, re-add it to the queue
if(endReason==AudioTrackEndReason.FINISHED && repeatMode != RepeatMode.OFF)
@@ -171,7 +195,8 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
{
if(!playFromDefault())
{
manager.getBot().getNowplayingHandler().onTrackUpdate(guildId, null, this);
manager.getBot().getNowplayingHandler().onTrackUpdate(null);
manager.getBot().getStatusMessageHandler().onTrackUpdate(guildId);
if(!manager.getBot().getConfig().getStay())
manager.getBot().closeAudioConnection(guildId);
// unpause, in the case when the player was paused and the track has been skipped.
@@ -186,23 +211,56 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
}
}
@Override
public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception)
{
if (
exception.getMessage().equals("Sign in to confirm you're not a bot")
|| exception.getMessage().equals("Please sign in")
)
LOGGER.error(
"Track {} has failed to play: {}"
+ "You will need to sign in to Google to play YouTube tracks. More info: https://jmusicbot.com/youtube-oauth2",
track.getIdentifier(),
exception.getMessage()
);
else
LOGGER.error("Track {} has failed to play", track.getIdentifier(), exception);
}
@Override
public void onTrackStart(AudioPlayer player, AudioTrack track)
{
votes.clear();
manager.getBot().getNowplayingHandler().onTrackUpdate(guildId, track, this);
manager.getBot().getNowplayingHandler().onTrackUpdate(track);
manager.getBot().getStatusMessageHandler().onTrackUpdate(guildId);
if(manager.getBot().getDataLogService() != null)
{
RequestMetadata rm = getRequestMetadata();
String source = rm.queueInfo == null ? null : rm.queueInfo.source;
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;
}
}
// Formatting
public Message getNowPlaying(JDA jda)
public MessageCreateData getNowPlaying(JDA jda)
{
if(isMusicPlaying(jda))
{
Guild guild = guild(jda);
AudioTrack track = audioPlayer.getPlayingTrack();
MessageBuilder mb = new MessageBuilder();
mb.append(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing in "+guild.getSelfMember().getVoiceState().getChannel().getAsMention()+"...**"));
MessageCreateBuilder mb = new MessageCreateBuilder();
mb.addContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing in "+guild.getSelfMember().getVoiceState().getChannel().getAsMention()+"...**"));
EmbedBuilder eb = new EmbedBuilder();
eb.setColor(guild.getSelfMember().getColor());
RequestMetadata rm = getRequestMetadata();
@@ -210,9 +268,9 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
{
User u = guild.getJDA().getUserById(rm.user.id);
if(u==null)
eb.setAuthor(rm.user.username + "#" + rm.user.discrim, null, rm.user.avatar);
eb.setAuthor(FormatUtil.formatUsername(rm.user), null, rm.user.avatar);
else
eb.setAuthor(u.getName() + "#" + u.getDiscriminator(), null, u.getEffectiveAvatarUrl());
eb.setAuthor(FormatUtil.formatUsername(u), null, u.getEffectiveAvatarUrl());
}
try
@@ -233,43 +291,114 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
eb.setFooter("Source: " + track.getInfo().author, null);
double progress = (double)audioPlayer.getPlayingTrack().getPosition()/track.getDuration();
eb.setDescription((audioPlayer.isPaused() ? JMusicBot.PAUSE_EMOJI : JMusicBot.PLAY_EMOJI)
eb.setDescription(getStatusEmoji()
+ " "+FormatUtil.progressBar(progress)
+ " `[" + FormatUtil.formatTime(track.getPosition()) + "/" + FormatUtil.formatTime(track.getDuration()) + "]` "
+ " `[" + 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.setEmbed(eb.build()).build();
return mb.setEmbeds(eb.build()).build();
}
else return null;
}
public Message getNoMusicPlaying(JDA jda)
public MessageCreateData getNoMusicPlaying(JDA jda)
{
Guild guild = guild(jda);
return new MessageBuilder()
return new MessageCreateBuilder()
.setContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing...**"))
.setEmbed(new EmbedBuilder()
.setEmbeds(new EmbedBuilder()
.setTitle("No music playing")
.setDescription(JMusicBot.STOP_EMOJI+" "+FormatUtil.progressBar(-1)+" "+FormatUtil.volumeIcon(audioPlayer.getVolume()))
.setDescription(STOP_EMOJI+" "+FormatUtil.progressBar(-1)+" "+FormatUtil.volumeIcon(audioPlayer.getVolume()))
.setColor(guild.getSelfMember().getColor())
.build()).build();
}
public String getTopicFormat(JDA jda)
public MessageCreateData getStatusMessage(JDA jda)
{
if(isMusicPlaying(jda))
Guild guild = guild(jda);
if(guild == null)
return null;
MessageCreateBuilder mb = new MessageCreateBuilder();
mb.setContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing...**"));
EmbedBuilder eb = new EmbedBuilder();
eb.setColor(guild.getSelfMember().getColor());
AudioTrack track = audioPlayer.getPlayingTrack();
if(isMusicPlaying(jda) && track != null)
{
long userid = getRequestMetadata().getOwner();
AudioTrack track = audioPlayer.getPlayingTrack();
String title = track.getInfo().title;
if(title==null || title.equals("Unknown Title"))
title = track.getInfo().uri;
return "**"+title+"** ["+(userid==0 ? "autoplay" : "<@"+userid+">")+"]"
+ "\n" + (audioPlayer.isPaused() ? JMusicBot.PAUSE_EMOJI : JMusicBot.PLAY_EMOJI) + " "
+ "[" + FormatUtil.formatTime(track.getDuration()) + "] "
+ FormatUtil.volumeIcon(audioPlayer.getVolume());
try
{
eb.setTitle(track.getInfo().title, track.getInfo().uri);
}
catch(Exception e)
{
eb.setTitle(track.getInfo().title);
}
}
else return "No music playing " + JMusicBot.STOP_EMOJI + " " + FormatUtil.volumeIcon(audioPlayer.getVolume());
else
{
eb.setTitle("No music playing");
}
StringBuilder desc = new StringBuilder();
if(isMusicPlaying(jda) && track != null)
{
double progress = (double)track.getPosition()/track.getDuration();
desc.append(getStatusEmoji())
.append(" ").append(FormatUtil.progressBar(progress))
.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
{
desc.append(STOP_EMOJI)
.append(" ").append(FormatUtil.progressBar(-1))
.append(" ").append(FormatUtil.volumeIcon(audioPlayer.getVolume()));
}
List<QueuedTrack> list = queue.getList();
if(!list.isEmpty())
{
QueuedTrack next = list.get(0);
String nextTitle = next.getTrack().getInfo().title;
String requester = next.getRequestMetadata() == null || next.getRequestMetadata().user == null
? null
: FormatUtil.formatUsername(next.getRequestMetadata().user);
desc.append("\n").append("Next: **").append(nextTitle).append("**");
if(requester != null && !requester.isEmpty())
desc.append(" (by ").append(requester).append(")");
}
long total = 0L;
for(QueuedTrack qt : list)
total += qt.getTrack().getDuration();
Settings settings = manager.getBot().getSettingsManager().getSettings(guildId);
desc.append("\n")
.append(manager.getBot().getConfig().getSuccess())
.append(" Queue | ").append(list.size()).append(" entries | `")
.append(TimeUtil.formatTime(total)).append("` | ")
.append(settings.getQueueType().getEmoji()).append(" `")
.append(settings.getQueueType().getUserFriendlyName()).append("`");
if(settings.getRepeatMode().getEmoji() != null)
desc.append(" | ").append(settings.getRepeatMode().getEmoji());
if(!guild.getSelfMember().getVoiceState().inAudioChannel())
desc.append("\n").append(manager.getBot().getConfig().getWarning()).append(" Disconnected");
eb.setDescription(FormatUtil.filter(desc.toString()));
return mb.setEmbeds(eb.build()).build();
}
public String getStatusEmoji()
{
return audioPlayer.isPaused() ? PAUSE_EMOJI : PLAY_EMOJI;
}
// Audio Send Handler methods
@@ -27,9 +27,11 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.exceptions.PermissionException;
import net.dv8tion.jda.api.exceptions.RateLimitedException;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.utils.messages.MessageEditData;
/**
*
@@ -54,7 +56,7 @@ public class NowplayingHandler
public void setLastNPMessage(Message m)
{
lastNP.put(m.getGuild().getIdLong(), new Pair<>(m.getTextChannel().getIdLong(), m.getIdLong()));
lastNP.put(m.getGuild().getIdLong(), new Pair<>(m.getChannel().getIdLong(), m.getIdLong()));
}
public void clearLastNPMessage(Guild guild)
@@ -81,7 +83,7 @@ public class NowplayingHandler
continue;
}
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
Message msg = handler.getNowPlaying(bot.getJDA());
MessageCreateData msg = handler.getNowPlaying(bot.getJDA());
if(msg==null)
{
msg = handler.getNoMusicPlaying(bot.getJDA());
@@ -89,7 +91,7 @@ public class NowplayingHandler
}
try
{
tc.editMessageById(pair.getValue(), msg).queue(m->{}, t -> lastNP.remove(guildId));
tc.editMessageById(pair.getValue(), MessageEditData.fromCreateData(msg)).queue(m->{}, t -> lastNP.remove(guildId));
}
catch(Exception e)
{
@@ -98,54 +100,18 @@ public class NowplayingHandler
}
toRemove.forEach(id -> lastNP.remove(id));
}
public void updateTopic(long guildId, AudioHandler handler, boolean wait)
{
Guild guild = bot.getJDA().getGuildById(guildId);
if(guild==null)
return;
Settings settings = bot.getSettingsManager().getSettings(guildId);
TextChannel tchan = settings.getTextChannel(guild);
if(tchan!=null && guild.getSelfMember().hasPermission(tchan, Permission.MANAGE_CHANNEL))
{
String otherText;
String topic = tchan.getTopic();
if(topic==null || topic.isEmpty())
otherText = "\u200B";
else if(topic.contains("\u200B"))
otherText = topic.substring(topic.lastIndexOf("\u200B"));
else
otherText = "\u200B\n "+topic;
String text = handler.getTopicFormat(bot.getJDA()) + otherText;
if(!text.equals(tchan.getTopic()))
{
try
{
// normally here if 'wait' was false, we'd want to queue, however,
// new discord ratelimits specifically limiting changing channel topics
// mean we don't want a backlog of changes piling up, so if we hit a
// ratelimit, we just won't change the topic this time
tchan.getManager().setTopic(text).complete(wait);
}
catch(PermissionException | RateLimitedException ignore) {}
}
}
}
// "event"-based methods
public void onTrackUpdate(long guildId, AudioTrack track, AudioHandler handler)
public void onTrackUpdate(AudioTrack track)
{
// update bot status if applicable
if(bot.getConfig().getSongInStatus())
{
if(track!=null && bot.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inVoiceChannel()).count()<=1)
if(track!=null && bot.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inAudioChannel()).count()<=1)
bot.getJDA().getPresence().setActivity(Activity.listening(track.getInfo().title));
else
bot.resetGame();
}
// update channel topic if applicable
updateTopic(guildId, handler, false);
}
public void onMessageDelete(Guild guild, long messageId)
@@ -15,13 +15,38 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.dunctebot.sourcemanagers.DuncteBotSources;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.BotConfig;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.jagrosh.jmusicbot.utils.YouTubeUtil;
import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import com.typesafe.config.Config;
import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.getyarn.GetyarnAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.nico.NicoAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager;
import com.sedmelluq.lava.extensions.youtuberotator.YoutubeIpRotatorSetup;
import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner;
import com.sedmelluq.lava.extensions.youtuberotator.planner.BalancingIpRoutePlanner;
import com.sedmelluq.lava.extensions.youtuberotator.planner.NanoIpRoutePlanner;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import dev.lavalink.youtube.clients.Web;
import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
/**
*
@@ -29,21 +54,83 @@ import net.dv8tion.jda.api.entities.Guild;
*/
public class PlayerManager extends DefaultAudioPlayerManager
{
private final static Logger LOGGER = LoggerFactory.getLogger(PlayerManager.class);
private final Bot bot;
public PlayerManager(Bot bot)
private final BotConfig config;
public PlayerManager(Bot bot, BotConfig config)
{
this.bot = bot;
this.config = config;
}
public void init()
{
TransformativeAudioSourceManager.createTransforms(bot.getConfig().getTransforms()).forEach(t -> registerSourceManager(t));
AudioSourceManagers.registerRemoteSources(this);
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
if (config.getYTRoutingPlanner() != YouTubeUtil.RoutingPlanner.NONE)
{
AbstractRoutePlanner routePlanner = YouTubeUtil.createRouterPlanner(config.getYTRoutingPlanner(), config.getYTIpBlocks());
YoutubeIpRotatorSetup rotator = new YoutubeIpRotatorSetup(routePlanner);
rotator.forConfiguration(yt.getHttpInterfaceManager(), false)
.withMainDelegateFilter(yt.getContextFilter())
.setup();
}
yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages());
registerSourceManager(yt);
registerSourceManager(SoundCloudAudioSourceManager.createDefault());
registerSourceManager(new BandcampAudioSourceManager());
registerSourceManager(new VimeoAudioSourceManager());
registerSourceManager(new TwitchStreamAudioSourceManager());
registerSourceManager(new BeamAudioSourceManager());
registerSourceManager(new GetyarnAudioSourceManager());
registerSourceManager(new NicoAudioSourceManager());
registerSourceManager(new HttpAudioSourceManager(MediaContainerRegistry.DEFAULT_REGISTRY));
AudioSourceManagers.registerLocalSource(this);
source(YoutubeAudioSourceManager.class).setPlaylistPageCount(10);
DuncteBotSources.registerAll(this, "en-US");
}
private YoutubeAudioSourceManager setupYoutubeAudioSourceManager()
{
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages());
// OAuth2 setup
if (bot.getConfig().useYoutubeOauth2())
{
String token = null;
try
{
token = Files.readString(OtherUtil.getPath("youtubetoken.txt"));
}
catch (NoSuchFileException e)
{
/* ignored */
}
catch (IOException e)
{
LOGGER.warn("Failed to read YouTube OAuth2 token file: {}", e.getMessage());
return yt;
}
LOGGER.debug("Read YouTube OAuth2 refresh token from youtubetoken.txt");
try
{
yt.useOauth2(token, false);
}
catch (Exception e)
{
LOGGER.warn("Failed to authorise with YouTube. If this issue persists, delete the youtubetoken.txt file to reauthorise.", e);
}
}
return yt;
}
public Bot getBot()
{
return bot;
@@ -15,9 +15,10 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import com.jagrosh.jmusicbot.queue.Queueable;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.entities.User;
/**
@@ -27,22 +28,22 @@ import net.dv8tion.jda.api.entities.User;
public class QueuedTrack implements Queueable
{
private final AudioTrack track;
public QueuedTrack(AudioTrack track, User owner)
{
this(track, new RequestMetadata(owner));
}
private final RequestMetadata requestMetadata;
public QueuedTrack(AudioTrack track, RequestMetadata rm)
{
this.track = track;
this.track.setUserData(rm);
this.track.setUserData(rm == null ? RequestMetadata.EMPTY : rm);
this.requestMetadata = rm;
if (this.track.isSeekable() && rm != null)
track.setPosition(rm.requestInfo.startTimestamp);
}
@Override
public long getIdentifier()
{
return track.getUserData(RequestMetadata.class).getOwner();
return requestMetadata.getOwner();
}
public AudioTrack getTrack()
@@ -50,9 +51,17 @@ public class QueuedTrack implements Queueable
return track;
}
public RequestMetadata getRequestMetadata()
{
return requestMetadata;
}
@Override
public String toString()
{
return "`[" + FormatUtil.formatTime(track.getDuration()) + "]` **" + track.getInfo().title + "** - <@" + track.getUserData(RequestMetadata.class).getOwner() + ">";
String entry = "`[" + TimeUtil.formatTime(track.getDuration()) + "]` ";
AudioTrackInfo trackInfo = track.getInfo();
entry = entry + (trackInfo.uri.startsWith("http") ? "[**" + trackInfo.title + "**]("+trackInfo.uri+")" : "**" + trackInfo.title + "**");
return entry + " - <@" + track.getUserData(RequestMetadata.class).getOwner() + ">";
}
}
@@ -15,40 +15,100 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.entities.User;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class RequestMetadata
{
public static final RequestMetadata EMPTY = new RequestMetadata(null);
public static final RequestMetadata EMPTY = new RequestMetadata(null, null, null);
public final UserInfo user;
public final RequestInfo requestInfo;
public final QueueInfo queueInfo;
public RequestMetadata(User user)
public RequestMetadata(User user, RequestInfo requestInfo, QueueInfo queueInfo)
{
this.user = user == null ? null : new UserInfo(user.getIdLong(), user.getName(), user.getDiscriminator(), user.getEffectiveAvatarUrl());
this.requestInfo = requestInfo;
this.queueInfo = queueInfo;
}
public long getOwner()
{
return user == null ? 0L : user.id;
}
public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event)
{
return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri),
new QueueInfo("PLAY", null, event.getArgs(), System.currentTimeMillis()));
}
public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event, String source, String playlistName, String searchQuery)
{
return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri),
new QueueInfo(source, playlistName, searchQuery, System.currentTimeMillis()));
}
public static RequestMetadata fromDefaultPlaylist(String playlistName)
{
return new RequestMetadata(null, new RequestInfo(playlistName, null),
new QueueInfo("DEFAULT_PLAYLIST", playlistName, null, System.currentTimeMillis()));
}
public class RequestInfo
public static class RequestInfo
{
public final String query, url;
private RequestInfo(String query, String url)
public final long startTimestamp;
public RequestInfo(String query, String url)
{
this(query, url, tryGetTimestamp(query));
}
private RequestInfo(String query, String url, long startTimestamp)
{
this.query = query;
this.url = url;
this.query = query;
this.startTimestamp = startTimestamp;
}
private static final Pattern youtubeTimestampPattern = Pattern.compile("youtu(?:\\.be|be\\..+)/.*\\?.*(?!.*list=)t=([\\dhms]+)");
private static long tryGetTimestamp(String url)
{
if(url == null || url.isEmpty())
return 0;
Matcher matcher = youtubeTimestampPattern.matcher(url);
return matcher.find() ? TimeUtil.parseUnitTime(matcher.group(1)) : 0;
}
}
public static class QueueInfo
{
public final String source;
public final String playlistName;
public final String searchQuery;
public final long queuedAt;
public QueueInfo(String source, String playlistName, String searchQuery, long queuedAt)
{
this.source = source;
this.playlistName = playlistName;
this.searchQuery = searchQuery;
this.queuedAt = queuedAt;
}
}
public class UserInfo
public static class UserInfo
{
public final long id;
public final String username, discrim, avatar;
@@ -0,0 +1,309 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.settings.Settings;
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.Message;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.requests.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.utils.messages.MessageEditData;
public class StatusMessageHandler
{
private static final Logger LOG = LoggerFactory.getLogger("StatusMessage");
private final Bot bot;
private final Map<Long, StatusState> states;
public StatusMessageHandler(Bot bot)
{
this.bot = bot;
this.states = new ConcurrentHashMap<>();
}
public void init()
{
if(!bot.getConfig().useStatusMessages())
return;
bot.getThreadpool().scheduleWithFixedDelay(() -> updateAll(), 0, 1, TimeUnit.SECONDS);
}
public void onMessageReceived(MessageReceivedEvent event)
{
if(!bot.getConfig().useStatusMessages())
return;
if(!event.isFromGuild())
return;
Guild guild = event.getGuild();
StatusState state = states.computeIfAbsent(guild.getIdLong(), id -> new StatusState());
synchronized(state)
{
MessageChannelUnion channelUnion = event.getChannel();
if(channelUnion == null || channelUnion.getType() != ChannelType.TEXT)
return;
TextChannel channel = channelUnion.asTextChannel();
boolean isCommand = isCommandMessage(event.getMessage(), guild);
Settings settings = bot.getSettingsManager().getSettings(guild);
if(isCommand && !event.getAuthor().isBot() && settings.getTextChannel(guild) == null)
state.lastCommandChannelId = channel.getIdLong();
Long statusChannelId = resolveStatusChannelId(guild, state);
if(statusChannelId == null || statusChannelId == 0L)
return;
if(isCommand && !event.getAuthor().isBot() && channel.getIdLong() == statusChannelId)
state.pendingRepost = true;
if(channel.getIdLong() != statusChannelId)
return;
if(state.messageId != 0L && event.getMessageIdLong() == state.messageId)
return;
if(state.pendingRepost)
{
long selfId = bot.getJDA().getSelfUser().getIdLong();
if(event.getAuthor().getIdLong() == selfId)
{
debug(guild, state, "pending_repost_trigger", "author_is_self", event.getMessageIdLong());
repostStatusMessage(guild, channel, state);
state.pendingRepost = false;
}
return;
}
// Avoid repost loops when the status message itself is created
if(event.getAuthor().getIdLong() == bot.getJDA().getSelfUser().getIdLong() && state.createInFlight)
return;
debug(guild, state, "repost_due_to_message", "message_author", event.getAuthor().getIdLong());
repostStatusMessage(guild, channel, state);
}
}
public void onMessageDelete(Guild guild, long messageId)
{
if(!bot.getConfig().useStatusMessages())
return;
StatusState state = states.get(guild.getIdLong());
if(state == null)
return;
synchronized(state)
{
if(state.createInFlight)
{
debug(guild, state, "delete_ignored_in_flight", "message", messageId);
return;
}
if(state.messageId == messageId)
state.messageId = 0L;
}
}
public void onTrackUpdate(long guildId)
{
if(!bot.getConfig().useStatusMessages())
return;
StatusState state = states.computeIfAbsent(guildId, id -> new StatusState());
state.forceUpdate = true;
}
private void updateAll()
{
if(!bot.getConfig().useStatusMessages())
return;
JDA jda = bot.getJDA();
if(jda == null)
return;
for(Map.Entry<Long, StatusState> entry : states.entrySet())
{
long guildId = entry.getKey();
StatusState state = entry.getValue();
synchronized(state)
{
Guild guild = jda.getGuildById(guildId);
if(guild == null)
continue;
TextChannel channel = resolveStatusChannel(guild, state);
if(channel == null)
continue;
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
if(handler == null)
continue;
if(state.createInFlight)
continue;
boolean isPlaying = handler.isMusicPlaying(jda);
if(!isPlaying && !state.forceUpdate)
continue;
if(state.pendingRepost && (state.messageId == 0L || state.channelId != channel.getIdLong()))
continue;
MessageCreateData statusMessage = handler.getStatusMessage(jda);
if(state.messageId == 0L || state.channelId != channel.getIdLong())
{
debug(guild, state, "repost_due_to_missing_or_channel", "channel", channel.getIdLong());
repostStatusMessage(guild, channel, state, statusMessage);
}
else
{
final long editingId = state.messageId;
channel.editMessageById(editingId, MessageEditData.fromCreateData(statusMessage))
.queue(m -> {}, t ->
{
debug(guild, state, "edit_failed", t, editingId);
if(state.messageId == editingId && shouldResetMessageId(t))
state.messageId = 0L;
});
}
state.forceUpdate = false;
}
}
}
private void repostStatusMessage(Guild guild, TextChannel channel, StatusState state)
{
MessageCreateData statusMessage = getStatusMessage(guild);
if(statusMessage == null)
return;
repostStatusMessage(guild, channel, state, statusMessage);
}
private void repostStatusMessage(Guild guild, TextChannel channel, StatusState state, MessageCreateData statusMessage)
{
if(state.createInFlight)
return;
state.createInFlight = true;
debug(guild, state, "repost_start", "channel", channel.getIdLong());
deleteOldStatusMessage(guild, state);
channel.sendMessage(statusMessage).queue(m ->
{
state.channelId = channel.getIdLong();
state.messageId = m.getIdLong();
state.createInFlight = false;
debug(guild, state, "repost_success", "message", m.getIdLong());
}, t ->
{
state.createInFlight = false;
debug(guild, state, "repost_failed", t, 0L);
});
}
private void deleteOldStatusMessage(Guild guild, StatusState state)
{
if(state.messageId == 0L)
return;
TextChannel oldChannel = guild.getTextChannelById(state.channelId);
if(oldChannel == null)
return;
oldChannel.deleteMessageById(state.messageId).queue(m -> {}, t -> debug(guild, state, "delete_failed", t, state.messageId));
}
private boolean shouldResetMessageId(Throwable throwable)
{
if(throwable instanceof ErrorResponseException)
{
ErrorResponse response = ((ErrorResponseException)throwable).getErrorResponse();
return response == ErrorResponse.UNKNOWN_MESSAGE || response == ErrorResponse.UNKNOWN_CHANNEL;
}
return false;
}
private void debug(Guild guild, StatusState state, String action, String key, long value)
{
if(LOG.isDebugEnabled())
LOG.debug("guild={} action={} {}={} msgId={} chanId={} pending={} inFlight={} force={}",
guild.getId(), action, key, value, state.messageId, state.channelId, state.pendingRepost, state.createInFlight, state.forceUpdate);
}
private void debug(Guild guild, StatusState state, String action, Throwable throwable, long messageId)
{
if(LOG.isDebugEnabled())
{
String err = throwable == null ? "null" : throwable.getClass().getSimpleName() + ": " + throwable.getMessage();
LOG.debug("guild={} action={} error={} msgId={} chanId={} pending={} inFlight={} force={}",
guild.getId(), action, err, messageId == 0L ? state.messageId : messageId,
state.channelId, state.pendingRepost, state.createInFlight, state.forceUpdate);
}
}
private MessageCreateData getStatusMessage(Guild guild)
{
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
if(handler == null)
return null;
return handler.getStatusMessage(bot.getJDA());
}
private TextChannel resolveStatusChannel(Guild guild, StatusState state)
{
Long id = resolveStatusChannelId(guild, state);
return id == null ? null : guild.getTextChannelById(id);
}
private Long resolveStatusChannelId(Guild guild, StatusState state)
{
Settings settings = bot.getSettingsManager().getSettings(guild);
if(settings == null)
return null;
if(settings.getTextChannel(guild) != null)
return settings.getTextChannel(guild).getIdLong();
return state.lastCommandChannelId == 0L ? null : state.lastCommandChannelId;
}
private boolean isCommandMessage(Message message, Guild guild)
{
String content = message.getContentRaw();
if(content == null || content.isEmpty())
return false;
Settings settings = bot.getSettingsManager().getSettings(guild);
String prefix = settings.getPrefix() != null ? settings.getPrefix() : bot.getConfig().getPrefix();
String altprefix = bot.getConfig().getAltPrefix();
if(prefix != null && prefix.equalsIgnoreCase("@mention"))
{
long botId = bot.getJDA().getSelfUser().getIdLong();
String mention = "<@" + botId + ">";
String mentionNick = "<@!" + botId + ">";
if(content.startsWith(mention) || content.startsWith(mentionNick))
return true;
}
if(prefix != null && !prefix.equalsIgnoreCase("@mention") && content.startsWith(prefix))
return true;
return altprefix != null && !altprefix.isEmpty() && content.startsWith(altprefix);
}
private static class StatusState
{
private long channelId = 0L;
private long messageId = 0L;
private long lastCommandChannelId = 0L;
private boolean pendingRepost = false;
private boolean forceUpdate = false;
private boolean createInFlight = false;
}
}
@@ -16,10 +16,10 @@
package com.jagrosh.jmusicbot.audio;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.typesafe.config.Config;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -16,6 +16,10 @@
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
import net.dv8tion.jda.api.Permission;
/**
@@ -24,8 +28,11 @@ import net.dv8tion.jda.api.Permission;
*/
public abstract class AdminCommand extends Command
{
public AdminCommand()
protected final Bot bot;
public AdminCommand(Bot bot)
{
this.bot = bot;
this.category = new Category("Admin", event ->
{
if(event.getAuthor().getId().equals(event.getClient().getOwnerId()))
@@ -36,4 +43,40 @@ public abstract class AdminCommand extends Command
});
this.guildOnly = true;
}
@Override
protected final void execute(CommandEvent event)
{
CommandLogContext.clear();
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
public abstract class LoggedCommand extends Command
{
protected final Bot bot;
public LoggedCommand(Bot bot)
{
this.bot = bot;
}
@Override
protected final void execute(CommandEvent event)
{
CommandLogContext.clear();
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -20,9 +20,11 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.exceptions.PermissionException;
/**
@@ -45,6 +47,7 @@ public abstract class MusicCommand extends Command
@Override
protected void execute(CommandEvent event)
{
CommandLogContext.clear();
Settings settings = event.getClient().getSettingsFor(event.getGuild());
TextChannel tchannel = settings.getTextChannel(event.getGuild());
if(tchannel!=null && !event.getTextChannel().equals(tchannel))
@@ -54,23 +57,26 @@ public abstract class MusicCommand extends Command
event.getMessage().delete().queue();
} catch(PermissionException ignore){}
event.replyInDm(event.getClient().getError()+" You can only use that command in "+tchannel.getAsMention()+"!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "wrong_text_channel"));
return;
}
bot.getPlayerManager().setUpHandler(event.getGuild()); // no point constantly checking for this later
if(bePlaying && !((AudioHandler)event.getGuild().getAudioManager().getSendingHandler()).isMusicPlaying(event.getJDA()))
{
event.reply(event.getClient().getError()+" There must be music playing to use that!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "no_music_playing"));
return;
}
if(beListening)
{
VoiceChannel current = event.getGuild().getSelfMember().getVoiceState().getChannel();
VoiceChannel current = (VoiceChannel) event.getGuild().getSelfMember().getVoiceState().getChannel();
if(current==null)
current = settings.getVoiceChannel(event.getGuild());
GuildVoiceState userState = event.getMember().getVoiceState();
if(!userState.inVoiceChannel() || userState.isDeafened() || (current!=null && !userState.getChannel().equals(current)))
if(!userState.inAudioChannel() || userState.isDeafened() || (current!=null && !userState.getChannel().equals(current)))
{
event.replyError("You must be listening in "+(current==null ? "a voice channel" : current.getAsMention())+" to use that!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "not_listening"));
return;
}
@@ -78,10 +84,11 @@ public abstract class MusicCommand extends Command
if(afkChannel != null && afkChannel.equals(userState.getChannel()))
{
event.replyError("You cannot use that command in an AFK channel!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "afk_channel"));
return;
}
if(!event.getGuild().getSelfMember().getVoiceState().inVoiceChannel())
if(!event.getGuild().getSelfMember().getVoiceState().inAudioChannel())
{
try
{
@@ -90,13 +97,41 @@ public abstract class MusicCommand extends Command
catch(PermissionException ex)
{
event.reply(event.getClient().getError()+" I am unable to connect to "+userState.getChannel().getAsMention()+"!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "cannot_connect"));
return;
}
}
}
doCommand(event);
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -16,6 +16,10 @@
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -23,9 +27,48 @@ import com.jagrosh.jdautilities.command.Command;
*/
public abstract class OwnerCommand extends Command
{
public OwnerCommand()
protected final Bot bot;
public OwnerCommand(Bot bot)
{
this.bot = bot;
this.category = new Category("Owner");
this.ownerCommand = true;
}
@Override
protected final void execute(CommandEvent event)
{
CommandLogContext.clear();
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.admin;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class PrefixCmd extends AdminCommand
{
public PrefixCmd(Bot bot)
{
super(bot);
this.name = "prefix";
this.help = "sets a server-specific prefix";
this.arguments = "<prefix|NONE>";
@@ -35,24 +38,30 @@ public class PrefixCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.replyError("Please include a prefix or NONE");
CommandLogContext.setError("missing_prefix");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getPrefix();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setPrefix(null);
event.replySuccess("Prefix cleared.");
JSONObject meta = new JSONObject().put("before", before).put("after", JSONObject.NULL);
CommandLogContext.setMeta(meta);
}
else
{
s.setPrefix(event.getArgs());
event.replySuccess("Custom prefix set to `" + event.getArgs() + "` on *" + event.getGuild().getName() + "*");
JSONObject meta = new JSONObject().put("before", before).put("after", event.getArgs());
CommandLogContext.setMeta(meta);
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2022 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.admin;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
* @author Wolfgang Schwendtbauer
*/
public class QueueTypeCmd extends AdminCommand
{
public QueueTypeCmd(Bot bot)
{
super(bot);
this.name = "queuetype";
this.help = "changes the queue type";
this.arguments = "[" + String.join("|", QueueType.getNames()) + "]";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
public void doCommand(CommandEvent event)
{
String args = event.getArgs();
QueueType value;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
QueueType before = settings.getQueueType();
if (args.isEmpty())
{
QueueType currentType = settings.getQueueType();
event.reply(currentType.getEmoji() + " Current queue type is: `" + currentType.getUserFriendlyName() + "`.");
CommandLogContext.setMeta(new JSONObject().put("current", currentType.name()));
return;
}
try
{
value = QueueType.valueOf(args.toUpperCase());
}
catch (IllegalArgumentException e)
{
event.replyError("Invalid queue type. Valid types are: [" + String.join("|", QueueType.getNames()) + "]");
CommandLogContext.setError("invalid_queue_type");
return;
}
if (settings.getQueueType() != value)
{
settings.setQueueType(value);
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
if (handler != null)
handler.setQueueType(value);
}
event.reply(value.getEmoji() + " Queue type was set to `" + value.getUserFriendlyName() + "`.");
CommandLogContext.setMeta(new JSONObject().put("before", before.name()).put("after", value.name()));
}
}
@@ -20,9 +20,11 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.entities.Role;
import org.json.JSONObject;
/**
*
@@ -32,6 +34,7 @@ public class SetdjCmd extends AdminCommand
{
public SetdjCmd(Bot bot)
{
super(bot);
this.name = "setdj";
this.help = "sets the DJ role for certain music commands";
this.arguments = "<rolename|NONE>";
@@ -39,30 +42,40 @@ public class SetdjCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a role name or NONE");
CommandLogContext.setError("missing_role");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getRole(event.getGuild()) == null ? null : s.getRole(event.getGuild()).getId();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setDJRole(null);
event.reply(event.getClient().getSuccess()+" DJ role cleared; Only Admins can use the DJ commands.");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
}
else
{
List<Role> list = FinderUtil.findRoles(event.getArgs(), event.getGuild());
if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" No Roles found matching \""+event.getArgs()+"\"");
CommandLogContext.setError("role_not_found");
}
else if (list.size()>1)
{
event.reply(event.getClient().getWarning()+FormatUtil.listOfRoles(list, event.getArgs()));
CommandLogContext.setError("role_ambiguous");
}
else
{
s.setDJRole(list.get(0));
event.reply(event.getClient().getSuccess()+" DJ commands can now be used by users with the **"+list.get(0).getName()+"** role.");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -20,9 +20,11 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.json.JSONObject;
/**
*
@@ -32,6 +34,7 @@ public class SettcCmd extends AdminCommand
{
public SettcCmd(Bot bot)
{
super(bot);
this.name = "settc";
this.help = "sets the text channel for music commands";
this.arguments = "<channel|NONE>";
@@ -39,30 +42,41 @@ public class SettcCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a text channel or NONE");
CommandLogContext.setError("missing_text_channel");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getTextChannel(event.getGuild()) == null ? null : s.getTextChannel(event.getGuild()).getId();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setTextChannel(null);
event.reply(event.getClient().getSuccess()+" Music commands can now be used in any channel");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
}
else
{
List<TextChannel> list = FinderUtil.findTextChannels(event.getArgs(), event.getGuild());
@SuppressWarnings({"unchecked", "rawtypes"})
List<TextChannel> list = (List) FinderUtil.findTextChannels(event.getArgs(), event.getGuild());
if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" No Text Channels found matching \""+event.getArgs()+"\"");
CommandLogContext.setError("text_channel_not_found");
}
else if (list.size()>1)
{
event.reply(event.getClient().getWarning()+FormatUtil.listOfTChannels(list, event.getArgs()));
CommandLogContext.setError("text_channel_ambiguous");
}
else
{
s.setTextChannel(list.get(0));
event.reply(event.getClient().getSuccess()+" Music commands can now only be used in <#"+list.get(0).getId()+">");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -20,9 +20,11 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.json.JSONObject;
/**
*
@@ -32,6 +34,7 @@ public class SetvcCmd extends AdminCommand
{
public SetvcCmd(Bot bot)
{
super(bot);
this.name = "setvc";
this.help = "sets the voice channel for playing music";
this.arguments = "<channel|NONE>";
@@ -39,30 +42,41 @@ public class SetvcCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a voice channel or NONE");
CommandLogContext.setError("missing_voice_channel");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getVoiceChannel(event.getGuild()) == null ? null : s.getVoiceChannel(event.getGuild()).getId();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setVoiceChannel(null);
event.reply(event.getClient().getSuccess()+" Music can now be played in any channel");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
}
else
{
List<VoiceChannel> list = FinderUtil.findVoiceChannels(event.getArgs(), event.getGuild());
@SuppressWarnings({"unchecked", "rawtypes"})
List<VoiceChannel> list = (List) FinderUtil.findVoiceChannels(event.getArgs(), event.getGuild());
if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" No Voice Channels found matching \""+event.getArgs()+"\"");
CommandLogContext.setError("voice_channel_not_found");
}
else if (list.size()>1)
{
event.reply(event.getClient().getWarning()+FormatUtil.listOfVChannels(list, event.getArgs()));
CommandLogContext.setError("voice_channel_ambiguous");
}
else
{
s.setVoiceChannel(list.get(0));
event.reply(event.getClient().getSuccess()+" Music can now only be played in "+list.get(0).getAsMention());
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.admin;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SkipratioCmd extends AdminCommand
{
public SkipratioCmd(Bot bot)
{
super(bot);
this.name = "setskip";
this.help = "sets a server-specific skip percentage";
this.arguments = "<0 - 100>";
@@ -35,7 +38,7 @@ public class SkipratioCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
try
{
@@ -43,15 +46,21 @@ public class SkipratioCmd extends AdminCommand
if( val < 0 || val > 100)
{
event.replyError("The provided value must be between 0 and 100!");
CommandLogContext.setError("invalid_skip_ratio");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
double before = s.getSkipRatio();
s.setSkipRatio(val / 100.0);
event.replySuccess("Skip percentage has been set to `" + val + "%` of listeners on *" + event.getGuild().getName() + "*");
CommandLogContext.setMeta(new JSONObject()
.put("before", before)
.put("after", s.getSkipRatio()));
}
catch(NumberFormatException ex)
{
event.replyError("Please include an integer between 0 and 100 (default is 55). This number is the percentage of listening users that must vote to skip a song.");
CommandLogContext.setError("invalid_skip_ratio");
}
}
}
@@ -21,9 +21,12 @@ import com.jagrosh.jdautilities.menu.OrderedMenu;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import org.json.JSONObject;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -52,6 +55,7 @@ public class ForceRemoveCmd extends DJCommand
if (event.getArgs().isEmpty())
{
event.replyError("You need to mention a user!");
CommandLogContext.setError("missing_user");
return;
}
@@ -59,6 +63,7 @@ public class ForceRemoveCmd extends DJCommand
if (handler.getQueue().isEmpty())
{
event.replyError("There is nothing in the queue!");
CommandLogContext.setError("empty_queue");
return;
}
@@ -69,6 +74,7 @@ public class ForceRemoveCmd extends DJCommand
if(found.isEmpty())
{
event.replyError("Unable to find the user!");
CommandLogContext.setError("user_not_found");
return;
}
else if(found.size()>1)
@@ -106,14 +112,26 @@ public class ForceRemoveCmd extends DJCommand
private void removeAllEntries(User target, CommandEvent event)
{
int count = ((AudioHandler) event.getGuild().getAudioManager().getSendingHandler()).getQueue().removeAll(target.getIdLong());
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
int sizeBefore = handler.getQueue().size();
int count = handler.getQueue().removeAll(target.getIdLong());
if (count == 0)
{
event.replyWarning("**"+target.getName()+"** doesn't have any songs in the queue!");
CommandLogContext.setError("no_entries_for_user");
}
else
{
event.replySuccess("Successfully removed `"+count+"` entries from **"+target.getName()+"**#"+target.getDiscriminator()+".");
event.replySuccess("Successfully removed `"+count+"` entries from "+FormatUtil.formatUsername(target)+".");
}
JSONObject meta = new JSONObject()
.put("target_user_id", target.getIdLong())
.put("removed_count", count)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), null, "REMOVE_ALL",
"FORCE", null, null, null, meta.toString());
}
}
@@ -20,7 +20,9 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.DJCommand;
import net.dv8tion.jda.api.entities.User;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -43,7 +45,14 @@ public class ForceskipCmd extends DJCommand
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
RequestMetadata rm = handler.getRequestMetadata();
event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title
+"** "+(rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + rm.user.username + "**)"));
+"** "+(rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)"));
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject().put("reason", "FORCE");
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_FORCE", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.getPlayer().stopTrack();
}
}
@@ -6,7 +6,9 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.queue.FairQueue;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.queue.AbstractQueue;
import org.json.JSONObject;
/**
* Command that provides users the ability to move a track in the playlist.
@@ -34,6 +36,7 @@ public class MoveTrackCmd extends DJCommand
if(parts.length < 2)
{
event.replyError("Please include two valid indexes.");
CommandLogContext.setError("missing_indexes");
return;
}
@@ -46,40 +49,54 @@ public class MoveTrackCmd extends DJCommand
catch (NumberFormatException e)
{
event.replyError("Please provide two valid indexes.");
CommandLogContext.setError("invalid_indexes");
return;
}
if (from == to)
{
event.replyError("Can't move a track to the same position.");
CommandLogContext.setError("same_position");
return;
}
// Validate that from and to are available
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
FairQueue<QueuedTrack> queue = handler.getQueue();
AbstractQueue<QueuedTrack> queue = handler.getQueue();
if (isUnavailablePosition(queue, from))
{
String reply = String.format("`%d` is not a valid position in the queue!", from);
event.replyError(reply);
CommandLogContext.setError("invalid_from_position");
return;
}
if (isUnavailablePosition(queue, to))
{
String reply = String.format("`%d` is not a valid position in the queue!", to);
event.replyError(reply);
CommandLogContext.setError("invalid_to_position");
return;
}
// Move the track
int sizeBefore = queue.size();
QueuedTrack track = queue.moveItem(from - 1, to - 1);
String trackTitle = track.getTrack().getInfo().title;
String reply = String.format("Moved **%s** from position `%d` to `%d`.", trackTitle, from, to);
event.replySuccess(reply);
JSONObject meta = new JSONObject()
.put("position_from", from)
.put("position_to", to)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", queue.size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), track.getTrack(),
"MOVE", null, to, null, null, meta.toString());
}
private static boolean isUnavailablePosition(FairQueue<QueuedTrack> queue, int position)
private static boolean isUnavailablePosition(AbstractQueue<QueuedTrack> queue, int position)
{
return (position < 1 || position > queue.size());
}
}
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -42,9 +44,16 @@ public class PauseCmd extends DJCommand
if(handler.getPlayer().isPaused())
{
event.replyWarning("The player is already paused! Use `"+event.getClient().getPrefix()+"play` to unpause!");
CommandLogContext.setError("already_paused");
return;
}
handler.getPlayer().setPaused(true);
event.replySuccess("Paused **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**. Type `"+event.getClient().getPrefix()+"play` to unpause!");
JSONObject meta = new JSONObject()
.put("position_ms", handler.getPlayer().getPlayingTrack().getPosition());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"PAUSE", null, null, null, null, meta.toString());
}
}
@@ -19,13 +19,17 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.entities.Message;
import org.json.JSONObject;
/**
*
@@ -50,9 +54,11 @@ public class PlaynextCmd extends DJCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
{
event.replyWarning("Please include a song title or URL!");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "missing_query"));
return;
}
String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">")
@@ -79,14 +85,19 @@ public class PlaynextCmd extends DJCommand
if(bot.getConfig().isTooLong(track))
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
+ TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrackToFront(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "PLAYNEXT", null, event.getArgs());
int pos = handler.addTrackToFront(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "PLAYNEXT", pos, event.getArgs(), null);
String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
+"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
m.editMessage(addMsg).queue();
logCommandEvent(event, "OK", new JSONObject().put("queue_position", pos));
}
@Override
@@ -112,7 +123,10 @@ public class PlaynextCmd extends DJCommand
public void noMatches()
{
if(ytsearch)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_matches"));
}
else
bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+event.getArgs(), new ResultHandler(m,event,true));
}
@@ -124,6 +138,15 @@ public class PlaynextCmd extends DJCommand
m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
else
m.editMessage(event.getClient().getError()+" Error loading track.").queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", throwable.getMessage()));
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -18,8 +18,10 @@ package com.jagrosh.jmusicbot.commands.dj;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -37,13 +39,13 @@ public class RepeatCmd extends DJCommand
this.guildOnly = true;
}
// override musiccommand's execute because we don't actually care where this is used
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String args = event.getArgs();
RepeatMode value;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
RepeatMode before = settings.getRepeatMode();
if(args.isEmpty())
{
if(settings.getRepeatMode() == RepeatMode.OFF)
@@ -66,12 +68,11 @@ public class RepeatCmd extends DJCommand
else
{
event.replyError("Valid options are `off`, `all` or `single` (or leave empty to toggle between `off` and `all`)");
CommandLogContext.setError("invalid_repeat_mode");
return;
}
settings.setRepeatMode(value);
event.replySuccess("Repeat mode is now `"+value.getUserFriendlyName()+"`");
CommandLogContext.setMeta(new JSONObject().put("before", before.name()).put("after", value.name()));
}
@Override
public void doCommand(CommandEvent event) { /* Intentionally Empty */ }
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -47,16 +49,27 @@ public class SkiptoCmd extends DJCommand
catch(NumberFormatException e)
{
event.reply(event.getClient().getError()+" `"+event.getArgs()+"` is not a valid integer!");
CommandLogContext.setError("invalid_index");
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
if(index<1 || index>handler.getQueue().size())
{
event.reply(event.getClient().getError()+" Position must be a valid integer between 1 and "+handler.getQueue().size()+"!");
CommandLogContext.setError("index_out_of_range");
return;
}
int sizeBefore = handler.getQueue().size();
handler.getQueue().skip(index-1);
event.reply(event.getClient().getSuccess()+" Skipped to **"+handler.getQueue().get(0).getTrack().getInfo().title+"**");
JSONObject meta = new JSONObject()
.put("position_to", index)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_SKIPTO", null, index, null, null, meta.toString());
handler.getPlayer().stopTrack();
}
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -39,6 +41,16 @@ public class StopCmd extends DJCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int sizeBefore = handler.getQueue().size();
if(handler.getPlayer().getPlayingTrack() != null && bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject()
.put("queue_size_before", sizeBefore)
.put("queue_size_after", 0);
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"STOP", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.stopAndClear();
event.getGuild().getAudioManager().closeAudioConnection();
event.reply(event.getClient().getSuccess()+" The player has stopped and the queue has been cleared.");
@@ -19,8 +19,10 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -46,6 +48,7 @@ public class VolumeCmd extends DJCommand
if(event.getArgs().isEmpty())
{
event.reply(FormatUtil.volumeIcon(volume)+" Current volume is `"+volume+"`");
CommandLogContext.setMeta(new JSONObject().put("current", volume));
}
else
{
@@ -56,12 +59,16 @@ public class VolumeCmd extends DJCommand
nvolume = -1;
}
if(nvolume<0 || nvolume>150)
{
event.reply(event.getClient().getError()+" Volume must be a valid integer between 0 and 150!");
CommandLogContext.setError("invalid_volume");
}
else
{
handler.getPlayer().setVolume(nvolume);
settings.setVolume(nvolume);
event.reply(FormatUtil.volumeIcon(nvolume)+" Volume changed from `"+volume+"` to `"+nvolume+"`");
CommandLogContext.setMeta(new JSONObject().put("before", volume).put("after", nvolume));
}
}
}
@@ -0,0 +1,201 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 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, 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)
{
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);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.general;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.examples.command.PingCommand;
import com.jagrosh.jmusicbot.Bot;
import org.json.JSONObject;
public class LoggedPingCmd extends PingCommand
{
private final Bot bot;
public LoggedPingCmd(Bot bot)
{
super();
this.bot = bot;
}
@Override
protected void execute(CommandEvent event)
{
super.execute(event);
if(bot.getDataLogService() != null && event.getGuild() != null)
{
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), "OK", new JSONObject().put("ping", true).toString());
}
}
}
@@ -15,28 +15,32 @@
*/
package com.jagrosh.jmusicbot.commands.general;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.LoggedCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.json.JSONObject;
/**
*
* @author John Grosh <john.a.grosh@gmail.com>
*/
public class SettingsCmd extends Command
public class SettingsCmd extends LoggedCommand
{
private final static String EMOJI = "\uD83C\uDFA7"; // 🎧
public SettingsCmd(Bot bot)
{
super(bot);
this.name = "settings";
this.help = "shows the bots settings";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -44,15 +48,27 @@ public class SettingsCmd extends Command
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
Settings s = event.getClient().getSettingsFor(event.getGuild());
MessageBuilder builder = new MessageBuilder()
.append(EMOJI + " **")
.append(FormatUtil.filter(event.getSelfUser().getName()))
.append("** settings:");
JSONObject meta = new JSONObject()
.put("repeat_mode", s.getRepeatMode().name())
.put("queue_type", s.getQueueType().name())
.put("has_default_playlist", s.getDefaultPlaylist() != null)
.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("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 + " **")
.addContent(FormatUtil.filter(event.getSelfUser().getName()))
.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())
@@ -63,12 +79,17 @@ public class SettingsCmd extends Command
+ "\nRepeat Mode: " + (s.getRepeatMode() == RepeatMode.OFF
? s.getRepeatMode().getUserFriendlyName()
: "**"+s.getRepeatMode().getUserFriendlyName()+"**")
+ "\nQueue Type: " + (s.getQueueType() == QueueType.FAIR
? 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().inVoiceChannel()).count()
+ event.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inAudioChannel()).count()
+ " audio connections", null);
event.getChannel().sendMessage(builder.setEmbed(ebuilder.build()).build()).queue();
event.getMessage().getChannel().sendMessage(builder.setEmbeds(ebuilder.build()).build()).queue();
}
}
@@ -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;
}
}
}
@@ -20,8 +20,10 @@ import com.jagrosh.jlyrics.LyricsClient;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import org.json.JSONObject;
/**
*
@@ -44,6 +46,7 @@ public class LyricsCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
String title;
if(event.getArgs().isEmpty())
{
@@ -53,6 +56,7 @@ public class LyricsCmd extends MusicCommand
else
{
event.replyError("There must be music playing to use that!");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_music_playing"));
return;
}
}
@@ -64,6 +68,7 @@ public class LyricsCmd extends MusicCommand
if(lyrics == null)
{
event.replyError("Lyrics for `" + title + "` could not be found!" + (event.getArgs().isEmpty() ? " Try entering the song name manually (`lyrics [song name]`)" : ""));
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "lyrics_not_found"));
return;
}
@@ -95,6 +100,15 @@ public class LyricsCmd extends MusicCommand
}
else
event.reply(eb.setDescription(lyrics.getContent()).build());
logCommandEvent(event, "OK", new JSONObject().put("title", lyrics.getTitle()));
});
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -19,8 +19,10 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import org.json.JSONObject;
/**
*
@@ -41,15 +43,19 @@ public class NowplayingCmd extends MusicCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
Message m = handler.getNowPlaying(event.getJDA());
MessageCreateData m = handler.getNowPlaying(event.getJDA());
if(m==null)
{
event.reply(handler.getNoMusicPlaying(event.getJDA()));
event.getMessage().getChannel().sendMessage(handler.getNoMusicPlaying(event.getJDA())).queue();
bot.getNowplayingHandler().clearLastNPMessage(event.getGuild());
CommandLogContext.setMeta(new JSONObject().put("playing", false));
}
else
{
event.reply(m, msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
event.getMessage().getChannel().sendMessage(m).queue(msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
CommandLogContext.setMeta(new JSONObject()
.put("playing", true)
.put("title", handler.getPlayer().getPlayingTrack() == null ? JSONObject.NULL : handler.getPlayer().getPlayingTrack().getInfo().title));
}
}
}
@@ -15,6 +15,8 @@
*/
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
@@ -28,12 +30,14 @@ import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import java.util.concurrent.TimeUnit;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.exceptions.PermissionException;
import org.json.JSONObject;
/**
*
@@ -62,6 +66,7 @@ public class PlayCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
@@ -71,9 +76,20 @@ public class PlayCmd extends MusicCommand
{
handler.getPlayer().setPaused(false);
event.replySuccess("Resumed **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**.");
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject()
.put("position_ms", handler.getPlayer().getPlayingTrack().getPosition());
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"RESUME", null, null, null, null, meta.toString());
logCommandEvent(event, getName(), event.getArgs(), "OK", meta);
}
}
else
{
event.replyError("Only DJs can unpause the player!");
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "not_dj"));
}
return;
}
StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Play Commands:\n");
@@ -82,6 +98,7 @@ public class PlayCmd extends MusicCommand
for(Command cmd: children)
builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ").append(cmd.getName()).append(" ").append(cmd.getArguments()).append("` - ").append(cmd.getHelp());
event.reply(builder.toString());
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject().put("help", true));
return;
}
String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">")
@@ -108,13 +125,17 @@ public class PlayCmd extends MusicCommand
if(bot.getConfig().isTooLong(track))
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
+ TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "PLAY", null, event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "PLAY", pos, event.getArgs(), null);
String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
+"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
if(playlist==null || !event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ADD_REACTION))
m.editMessage(addMsg).queue();
else
@@ -135,6 +156,7 @@ public class PlayCmd extends MusicCommand
try{ m.clearReactions().queue(); }catch(PermissionException ignore) {}
}).build().display(m);
}
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject().put("queue_position", pos));
}
private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude)
@@ -144,7 +166,10 @@ public class PlayCmd extends MusicCommand
if(!bot.getConfig().isTooLong(track) && !track.equals(exclude))
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
handler.addTrack(new QueuedTrack(track, event.getAuthor()));
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "PLAYLIST", playlist == null ? null : playlist.getName(), event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "PLAYLIST", pos, event.getArgs(), playlist == null ? null : playlist.getName());
count[0]++;
}
});
@@ -173,10 +198,17 @@ public class PlayCmd extends MusicCommand
else
{
int count = loadPlaylist(playlist, null);
if(count==0)
if(playlist.getTracks().size() == 0)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" The playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName()
+"**) ")+" could not be loaded or contained 0 entries")).queue();
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "playlist_empty"));
}
else if(count==0)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" All entries in this playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName()
+"**) ")+"were longer than the allowed maximum (`"+bot.getConfig().getMaxTime()+"`)")).queue();
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "playlist_entries_too_long"));
}
else
{
@@ -185,6 +217,7 @@ public class PlayCmd extends MusicCommand
+ playlist.getTracks().size()+"` entries; added to the queue!"
+ (count<playlist.getTracks().size() ? "\n"+event.getClient().getWarning()+" Tracks longer than the allowed maximum (`"
+ bot.getConfig().getMaxTime()+"`) have been omitted." : ""))).queue();
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject().put("playlist_name", playlist.getName()).put("added_count", count));
}
}
}
@@ -193,7 +226,13 @@ public class PlayCmd extends MusicCommand
public void noMatches()
{
if(ytsearch)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", "no_matches").toString());
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "no_matches"));
}
else
bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+event.getArgs(), new ResultHandler(m,event,true));
}
@@ -205,6 +244,10 @@ public class PlayCmd extends MusicCommand
m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
else
m.editMessage(event.getClient().getError()+" Error loading track.").queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", throwable.getMessage()).toString());
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", throwable.getMessage()));
}
}
@@ -224,21 +267,30 @@ public class PlayCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a playlist name.");
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "missing_playlist"));
return;
}
Playlist playlist = bot.getPlaylistLoader().getPlaylist(event.getArgs());
if(playlist==null)
{
event.replyError("I could not find `"+event.getArgs()+".txt` in the Playlists folder.");
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "playlist_not_found"));
return;
}
event.getChannel().sendMessage(loadingEmoji+" Loading playlist **"+event.getArgs()+"**... ("+playlist.getItems().size()+" items)").queue(m ->
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
playlist.loadTracks(bot.getPlayerManager(), (at)->handler.addTrack(new QueuedTrack(at, event.getAuthor())), () -> {
playlist.loadTracks(bot.getPlayerManager(), (at) ->
{
RequestMetadata rm = RequestMetadata.fromResultHandler(at, event, "PLAYLIST", event.getArgs(), event.getArgs());
int pos = handler.addTrack(new QueuedTrack(at, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), at, "PLAYLIST", pos, event.getArgs(), event.getArgs());
}, () -> {
StringBuilder builder = new StringBuilder(playlist.getTracks().isEmpty()
? event.getClient().getWarning()+" No tracks were loaded!"
: event.getClient().getSuccess()+" Loaded **"+playlist.getTracks().size()+"** tracks!");
@@ -249,8 +301,25 @@ public class PlayCmd extends MusicCommand
if(str.length()>2000)
str = str.substring(0,1994)+" (...)";
m.editMessage(FormatUtil.filter(str)).queue();
if(bot.getDataLogService() != null)
{
playlist.getErrors().forEach(err ->
bot.getDataLogService().logPlaylistError(event.getGuild(), event.getAuthor(), event.getArgs(), err.getItem(), err.getReason()));
}
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject()
.put("playlist_name", event.getArgs())
.put("loaded_count", playlist.getTracks().size())
.put("error_count", playlist.getErrors().size()));
});
});
}
}
private void logCommandEvent(CommandEvent event, String commandName, String args, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), commandName,
args, result, meta == null ? null : meta.toString());
}
}
@@ -19,6 +19,8 @@ import java.util.List;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -45,19 +47,27 @@ public class PlaylistsCmd extends MusicCommand
if(!bot.getPlaylistLoader().folderExists())
{
event.reply(event.getClient().getWarning()+" Playlists folder does not exist and could not be created!");
CommandLogContext.setError("playlist_folder_missing");
return;
}
List<String> list = bot.getPlaylistLoader().getPlaylistNames();
if(list==null)
{
event.reply(event.getClient().getError()+" Failed to load available playlists!");
CommandLogContext.setError("playlist_list_failed");
}
else if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" There are no playlists in the Playlists folder!");
CommandLogContext.setMeta(new JSONObject().put("count", 0));
}
else
{
StringBuilder builder = new StringBuilder(event.getClient().getSuccess()+" Available playlists:\n");
list.forEach(str -> builder.append("`").append(str).append("` "));
builder.append("\nType `").append(event.getClient().getTextualPrefix()).append("play playlist <name>` to play a playlist");
event.reply(builder.toString());
CommandLogContext.setMeta(new JSONObject().put("count", list.size()));
}
}
}
@@ -20,17 +20,21 @@ import java.util.concurrent.TimeUnit;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.menu.Paginator;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.JMusicBot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.MessageBuilder;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.exceptions.PermissionException;
import org.json.JSONObject;
/**
*
@@ -74,16 +78,17 @@ public class QueueCmd extends MusicCommand
List<QueuedTrack> list = ah.getQueue().getList();
if(list.isEmpty())
{
Message nowp = ah.getNowPlaying(event.getJDA());
Message nonowp = ah.getNoMusicPlaying(event.getJDA());
Message built = new MessageBuilder()
MessageCreateData nowp = ah.getNowPlaying(event.getJDA());
MessageCreateData nonowp = ah.getNoMusicPlaying(event.getJDA());
MessageCreateData built = new MessageCreateBuilder()
.setContent(event.getClient().getWarning() + " There is no music in the queue!")
.setEmbed((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build();
event.reply(built, m ->
.setEmbeds((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build();
event.getMessage().getChannel().sendMessage(built).queue(m ->
{
if(nowp!=null)
bot.getNowplayingHandler().setLastNPMessage(m);
});
CommandLogContext.setMeta(new JSONObject().put("queue_size", 0));
return;
}
String[] songs = new String[list.size()];
@@ -95,7 +100,12 @@ public class QueueCmd extends MusicCommand
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
long fintotal = total;
builder.setText((i1,i2) -> getQueueTitle(ah, event.getClient().getSuccess(), songs.length, fintotal, settings.getRepeatMode()))
CommandLogContext.setMeta(new JSONObject()
.put("queue_size", list.size())
.put("total_ms", total)
.put("repeat_mode", settings.getRepeatMode().name())
.put("queue_type", settings.getQueueType().name()));
builder.setText((i1,i2) -> getQueueTitle(ah, event.getClient().getSuccess(), songs.length, fintotal, settings.getRepeatMode(), settings.getQueueType()))
.setItems(songs)
.setUsers(event.getAuthor())
.setColor(event.getSelfMember().getColor())
@@ -103,16 +113,17 @@ public class QueueCmd extends MusicCommand
builder.build().paginate(event.getChannel(), pagenum);
}
private String getQueueTitle(AudioHandler ah, String success, int songslength, long total, RepeatMode repeatmode)
private String getQueueTitle(AudioHandler ah, String success, int songslength, long total, RepeatMode repeatmode, QueueType queueType)
{
StringBuilder sb = new StringBuilder();
if(ah.getPlayer().getPlayingTrack()!=null)
{
sb.append(ah.getPlayer().isPaused() ? JMusicBot.PAUSE_EMOJI : JMusicBot.PLAY_EMOJI).append(" **")
sb.append(ah.getStatusEmoji()).append(" **")
.append(ah.getPlayer().getPlayingTrack().getInfo().title).append("**\n");
}
return FormatUtil.filter(sb.append(success).append(" Current Queue | ").append(songslength)
.append(" entries | `").append(FormatUtil.formatTime(total)).append("` ")
.append(repeatmode.getEmoji() != null ? "| "+repeatmode.getEmoji() : "").toString());
.append(" entries | `").append(TimeUtil.formatTime(total)).append("` ")
.append("| ").append(queueType.getEmoji()).append(" `").append(queueType.getUserFriendlyName()).append('`')
.append(repeatmode.getEmoji() != null ? " | "+repeatmode.getEmoji() : "").toString());
}
}
@@ -20,9 +20,11 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.User;
import org.json.JSONObject;
/**
*
@@ -48,15 +50,28 @@ public class RemoveCmd extends MusicCommand
if(handler.getQueue().isEmpty())
{
event.replyError("There is nothing in the queue!");
CommandLogContext.setError("empty_queue");
return;
}
if(event.getArgs().equalsIgnoreCase("all"))
{
int sizeBefore = handler.getQueue().size();
int count = handler.getQueue().removeAll(event.getAuthor().getIdLong());
if(count==0)
{
event.replyWarning("You don't have any songs in the queue!");
CommandLogContext.setError("no_entries_for_user");
}
else
event.replySuccess("Successfully removed your "+count+" entries.");
JSONObject meta = new JSONObject()
.put("removed_count", count)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), null,
"REMOVE_ALL", "SELF", null, null, null, meta.toString());
return;
}
int pos;
@@ -68,6 +83,7 @@ public class RemoveCmd extends MusicCommand
if(pos<1 || pos>handler.getQueue().size())
{
event.replyError("Position must be a valid integer between 1 and "+handler.getQueue().size()+"!");
CommandLogContext.setError("invalid_position");
return;
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
@@ -79,6 +95,11 @@ public class RemoveCmd extends MusicCommand
{
handler.getQueue().remove(pos-1);
event.replySuccess("Removed **"+qt.getTrack().getInfo().title+"** from the queue");
JSONObject meta = new JSONObject().put("position", pos);
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), qt.getTrack(),
"REMOVE", "SELF", pos, null, null, meta.toString());
}
else if(isDJ)
{
@@ -91,10 +112,16 @@ public class RemoveCmd extends MusicCommand
}
event.replySuccess("Removed **"+qt.getTrack().getInfo().title
+"** from the queue (requested by "+(u==null ? "someone" : "**"+u.getName()+"**")+")");
JSONObject meta = new JSONObject().put("position", pos).put("removed_by_dj", true);
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), qt.getTrack(),
"REMOVE", "DJ", pos, null, null, meta.toString());
}
else
{
event.replyError("You cannot remove **"+qt.getTrack().getInfo().title+"** because you didn't add it!");
CommandLogContext.setError("not_owner");
}
}
}
@@ -15,6 +15,8 @@
*/
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
@@ -27,9 +29,11 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import org.json.JSONObject;
/**
*
@@ -62,9 +66,11 @@ public class SearchCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty())
{
event.replyError("Please include a query.");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "missing_query"));
return;
}
event.reply(searchingEmoji+" Searching... `["+event.getArgs()+"]`",
@@ -88,14 +94,22 @@ public class SearchCmd extends MusicCommand
if(bot.getConfig().isTooLong(track))
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue();
+ TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "SEARCH", null, event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
{
bot.getDataLogService().logSearchSelection(event.getGuild(), event.getAuthor(), event.getArgs(), 1, track);
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "SEARCH", pos, event.getArgs(), null);
}
m.editMessage(FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing"
+"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing"
: " to the queue at position "+pos))).queue();
logCommandEvent(event, "OK", new JSONObject().put("queue_position", pos).put("selected_index", 1));
}
@Override
@@ -110,13 +124,20 @@ public class SearchCmd extends MusicCommand
if(bot.getConfig().isTooLong(track))
{
event.replyWarning("This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`");
+ TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "SEARCH", null, event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
{
bot.getDataLogService().logSearchSelection(event.getGuild(), event.getAuthor(), event.getArgs(), i, track);
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "SEARCH", pos, event.getArgs(), null);
}
event.replySuccess("Added **" + FormatUtil.filter(track.getInfo().title)
+ "** (`" + FormatUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing"
+ "** (`" + TimeUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing"
: " to the queue at position "+pos));
})
.setCancel((msg) -> {})
@@ -125,15 +146,20 @@ public class SearchCmd extends MusicCommand
for(int i=0; i<4 && i<playlist.getTracks().size(); i++)
{
AudioTrack track = playlist.getTracks().get(i);
builder.addChoices("`["+FormatUtil.formatTime(track.getDuration())+"]` [**"+track.getInfo().title+"**]("+track.getInfo().uri+")");
builder.addChoices("`["+ TimeUtil.formatTime(track.getDuration())+"]` [**"+track.getInfo().title+"**]("+track.getInfo().uri+")");
}
builder.build().display(m);
logCommandEvent(event, "OK", new JSONObject().put("result_count", playlist.getTracks().size()));
}
@Override
public void noMatches()
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", "no_matches").toString());
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_matches"));
}
@Override
@@ -143,6 +169,18 @@ public class SearchCmd extends MusicCommand
m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
else
m.editMessage(event.getClient().getError()+" Error loading track.").queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", throwable.getMessage()).toString());
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", throwable.getMessage()));
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2020 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.json.JSONObject;
/**
* @author Whew., Inc.
*/
public class SeekCmd extends MusicCommand
{
private final static Logger LOG = LoggerFactory.getLogger("Seeking");
public SeekCmd(Bot bot)
{
super(bot);
this.name = "seek";
this.help = "seeks the current song";
this.arguments = "[+ | -] <HH:MM:SS | MM:SS | SS>|<0h0m0s | 0m0s | 0s>";
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = true;
}
@Override
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
AudioTrack playingTrack = handler.getPlayer().getPlayingTrack();
if (!playingTrack.isSeekable())
{
event.replyError("This track is not seekable.");
CommandLogContext.setError("not_seekable");
return;
}
if (!DJCommand.checkDJPermission(event) && playingTrack.getUserData(RequestMetadata.class).getOwner() != event.getAuthor().getIdLong())
{
event.replyError("You cannot seek **" + playingTrack.getInfo().title + "** because you didn't add it!");
CommandLogContext.setError("not_owner");
return;
}
String args = event.getArgs();
TimeUtil.SeekTime seekTime = TimeUtil.parseTime(args);
if (seekTime == null)
{
event.replyError("Invalid seek! Expected format: " + arguments + "\nExamples: `1:02:23` `+1:10` `-90`, `1h10m`, `+90s`");
CommandLogContext.setError("invalid_seek");
return;
}
long currentPosition = playingTrack.getPosition();
long trackDuration = playingTrack.getDuration();
long seekMilliseconds = seekTime.relative ? currentPosition + seekTime.milliseconds : seekTime.milliseconds;
if (seekMilliseconds > trackDuration)
{
event.replyError("Cannot seek to `" + TimeUtil.formatTime(seekMilliseconds) + "` because the current track is `" + TimeUtil.formatTime(trackDuration) + "` long!");
CommandLogContext.setError("seek_out_of_bounds");
return;
}
try
{
playingTrack.setPosition(seekMilliseconds);
}
catch (Exception e)
{
event.replyError("An error occurred while trying to seek: " + e.getMessage());
LOG.warn("Failed to seek track " + playingTrack.getIdentifier(), e);
CommandLogContext.setError("seek_failed");
return;
}
event.replySuccess("Successfully seeked to `" + TimeUtil.formatTime(playingTrack.getPosition()) + "/" + TimeUtil.formatTime(playingTrack.getDuration()) + "`!");
JSONObject meta = new JSONObject()
.put("seek_from_ms", currentPosition)
.put("seek_to_ms", playingTrack.getPosition());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), playingTrack,
"SEEK", null, null, null, null, meta.toString());
}
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -40,17 +42,28 @@ public class ShuffleCmd extends MusicCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int sizeBefore = handler.getQueue().size();
int s = handler.getQueue().shuffle(event.getAuthor().getIdLong());
switch (s)
{
case 0:
event.replyError("You don't have any music in the queue to shuffle!");
CommandLogContext.setError("no_entries_to_shuffle");
break;
case 1:
event.replyWarning("You only have one song in the queue!");
CommandLogContext.setMeta(new JSONObject().put("shuffled_count", 1));
break;
default:
event.replySuccess("You successfully shuffled your "+s+" entries.");
JSONObject meta = new JSONObject()
.put("shuffled_count", s)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), null,
"SHUFFLE", null, null, null, null, meta.toString());
break;
}
}
@@ -20,7 +20,9 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import net.dv8tion.jda.api.entities.User;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -43,9 +45,20 @@ public class SkipCmd extends MusicCommand
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
RequestMetadata rm = handler.getRequestMetadata();
if(event.getAuthor().getIdLong() == rm.getOwner())
double skipRatio = bot.getSettingsManager().getSettings(event.getGuild()).getSkipRatio();
if(skipRatio == -1) {
skipRatio = bot.getConfig().getSkipRatio();
}
if(event.getAuthor().getIdLong() == rm.getOwner() || skipRatio == 0)
{
event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**");
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject().put("reason", "REQUESTER");
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_REQUESTER", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.getPlayer().stopTrack();
}
else
@@ -59,15 +72,36 @@ public class SkipCmd extends MusicCommand
{
msg = event.getClient().getSuccess()+" You voted to skip the song `[";
handler.getVotes().add(event.getAuthor().getId());
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject().put("vote", "added");
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_VOTE", null, null, null, null, meta.toString());
}
}
int skippers = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream()
.filter(m -> handler.getVotes().contains(m.getUser().getId())).count();
int required = (int)Math.ceil(listeners * bot.getSettingsManager().getSettings(event.getGuild()).getSkipRatio());
int required = (int)Math.ceil(listeners * skipRatio);
msg += skippers + " votes, " + required + "/" + listeners + " needed]`";
CommandLogContext.setMeta(new JSONObject()
.put("votes", skippers)
.put("required", required)
.put("listeners", listeners));
if(skippers>=required)
{
msg += "\n" + event.getClient().getSuccess() + " Skipped **" + handler.getPlayer().getPlayingTrack().getInfo().title
+ "** " + (rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + rm.user.username + "**)");
+ "** " + (rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)");
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject()
.put("reason", "VOTE")
.put("votes", skippers)
.put("required", required)
.put("listeners", listeners);
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_VOTE_ACTION", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.getPlayer().stopTrack();
}
event.reply(msg);
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -26,11 +28,9 @@ import com.jagrosh.jmusicbot.settings.Settings;
*/
public class AutoplaylistCmd extends OwnerCommand
{
private final Bot bot;
public AutoplaylistCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.guildOnly = true;
this.name = "autoplaylist";
this.arguments = "<name|NONE>";
@@ -39,30 +39,34 @@ public class AutoplaylistCmd extends OwnerCommand
}
@Override
public void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a playlist name or NONE");
CommandLogContext.setError("missing_playlist");
return;
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
String before = settings.getDefaultPlaylist();
if(event.getArgs().equalsIgnoreCase("none"))
{
Settings settings = event.getClient().getSettingsFor(event.getGuild());
settings.setDefaultPlaylist(null);
event.reply(event.getClient().getSuccess()+" Cleared the default playlist for **"+event.getGuild().getName()+"**");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
return;
}
String pname = event.getArgs().replaceAll("\\s+", "_");
if(bot.getPlaylistLoader().getPlaylist(pname)==null)
{
event.reply(event.getClient().getError()+" Could not find `"+pname+".txt`!");
CommandLogContext.setError("playlist_not_found");
}
else
{
Settings settings = event.getClient().getSettingsFor(event.getGuild());
settings.setDefaultPlaylist(pname);
event.reply(event.getClient().getSuccess()+" The default playlist for **"+event.getGuild().getName()+"** is now `"+pname+"`");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", pname));
}
}
}
@@ -16,14 +16,16 @@
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;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary;
import net.dv8tion.jda.api.JDAInfo;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.ChannelType;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.utils.FileUpload;
import org.json.JSONObject;
/**
*
@@ -34,11 +36,9 @@ public class DebugCmd extends OwnerCommand
private final static String[] PROPERTIES = {"java.version", "java.vm.name", "java.vm.specification.version",
"java.runtime.name", "java.runtime.version", "java.specification.version", "os.arch", "os.name"};
private final Bot bot;
public DebugCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.name = "debug";
this.help = "shows debug info";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -46,8 +46,9 @@ public class DebugCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
CommandLogContext.setMeta(new JSONObject().put("channel_type", event.getChannelType().name()));
StringBuilder sb = new StringBuilder();
sb.append("```\nSystem Properties:");
for(String key: PROPERTIES)
@@ -65,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);
@@ -79,8 +80,8 @@ public class DebugCmd extends OwnerCommand
sb.append("\n```");
if(event.isFromType(ChannelType.PRIVATE)
|| event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ATTACH_FILES))
event.getChannel().sendFile(sb.toString().getBytes(), "debug_information.txt").queue();
|| event.getSelfMember().hasPermission(event.getGuildChannel(), Permission.MESSAGE_ATTACH_FILES))
event.getChannel().sendFiles(FileUpload.fromData(sb.toString().getBytes(), "debug_information.txt")).queue();
else
event.reply("Debug Information: " + sb.toString());
}
@@ -20,6 +20,9 @@ import javax.script.ScriptEngineManager;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import org.json.JSONObject;
/**
*
@@ -27,34 +30,55 @@ import com.jagrosh.jmusicbot.commands.OwnerCommand;
*/
public class EvalCmd extends OwnerCommand
{
private final Bot bot;
private final String engine;
public EvalCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.name = "eval";
this.help = "evaluates nashorn code";
this.aliases = bot.getConfig().getAliases(this.name);
this.engine = bot.getConfig().getEvalEngine();
this.guildOnly = false;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
ScriptEngine se = new ScriptEngineManager().getEngineByName("Nashorn");
CommandLogContext.setManualLogging();
ScriptEngine se = new ScriptEngineManager().getEngineByName(engine);
if(se == null)
{
event.replyError("The eval engine provided in the config (`"+engine+"`) doesn't exist. This could be due to an invalid "
+ "engine name, or the engine not existing in your version of java (`"+System.getProperty("java.version")+"`).");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "engine_not_found"));
return;
}
se.put("bot", bot);
se.put("event", event);
se.put("jda", event.getJDA());
se.put("guild", event.getGuild());
se.put("channel", event.getChannel());
if (event.getChannelType() != ChannelType.PRIVATE) {
se.put("guild", event.getGuild());
se.put("channel", event.getChannel());
}
try
{
event.reply(event.getClient().getSuccess()+" Evaluated Successfully:\n```\n"+se.eval(event.getArgs())+" ```");
logCommandEvent(event, "OK", new JSONObject().put("invoked", true));
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" An exception was thrown:\n```\n"+e+" ```");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "eval_exception"));
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
null, result, meta == null ? null : meta.toString());
}
}
@@ -17,11 +17,12 @@ package com.jagrosh.jmusicbot.commands.owner;
import java.io.IOException;
import java.util.List;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import org.json.JSONObject;
/**
*
@@ -29,10 +30,9 @@ import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
*/
public class PlaylistCmd extends OwnerCommand
{
private final Bot bot;
public PlaylistCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.guildOnly = false;
this.name = "playlist";
this.arguments = "<append|delete|make|setdefault>";
@@ -48,19 +48,21 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
public void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Playlist Management Commands:\n");
for(Command cmd: this.children)
for(com.jagrosh.jdautilities.command.Command cmd: this.children)
builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ").append(cmd.getName())
.append(" ").append(cmd.getArguments()==null ? "" : cmd.getArguments()).append("` - ").append(cmd.getHelp());
event.reply(builder.toString());
CommandLogContext.setMeta(new JSONObject().put("subcommands", this.children.length));
}
public class MakelistCmd extends OwnerCommand
{
public MakelistCmd()
{
super(PlaylistCmd.this.bot);
this.name = "make";
this.aliases = new String[]{"create"};
this.help = "makes a new playlist";
@@ -69,23 +71,34 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String pname = event.getArgs().replaceAll("\\s+", "_");
if(bot.getPlaylistLoader().getPlaylist(pname)==null)
pname = pname.replaceAll("[*?|\\/\":<>]", "");
if(pname == null || pname.isEmpty())
{
event.replyError("Please provide a name for the playlist!");
CommandLogContext.setError("missing_playlist_name");
}
else if(bot.getPlaylistLoader().getPlaylist(pname) == null)
{
try
{
bot.getPlaylistLoader().createPlaylist(pname);
event.reply(event.getClient().getSuccess()+" Successfully created playlist `"+pname+"`!");
CommandLogContext.setMeta(new JSONObject().put("playlist_name", pname));
}
catch(IOException e)
{
event.reply(event.getClient().getError()+" I was unable to create the playlist: "+e.getLocalizedMessage());
CommandLogContext.setError("create_playlist_failed");
}
}
else
{
event.reply(event.getClient().getError()+" Playlist `"+pname+"` already exists!");
CommandLogContext.setError("playlist_exists");
}
}
}
@@ -93,6 +106,7 @@ public class PlaylistCmd extends OwnerCommand
{
public DeletelistCmd()
{
super(PlaylistCmd.this.bot);
this.name = "delete";
this.aliases = new String[]{"remove"};
this.help = "deletes an existing playlist";
@@ -101,21 +115,26 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String pname = event.getArgs().replaceAll("\\s+", "_");
if(bot.getPlaylistLoader().getPlaylist(pname)==null)
{
event.reply(event.getClient().getError()+" Playlist `"+pname+"` doesn't exist!");
CommandLogContext.setError("playlist_not_found");
}
else
{
try
{
bot.getPlaylistLoader().deletePlaylist(pname);
event.reply(event.getClient().getSuccess()+" Successfully deleted playlist `"+pname+"`!");
CommandLogContext.setMeta(new JSONObject().put("playlist_name", pname));
}
catch(IOException e)
{
event.reply(event.getClient().getError()+" I was unable to delete the playlist: "+e.getLocalizedMessage());
CommandLogContext.setError("delete_playlist_failed");
}
}
}
@@ -125,6 +144,7 @@ public class PlaylistCmd extends OwnerCommand
{
public AppendlistCmd()
{
super(PlaylistCmd.this.bot);
this.name = "append";
this.aliases = new String[]{"add"};
this.help = "appends songs to an existing playlist";
@@ -133,18 +153,22 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String[] parts = event.getArgs().split("\\s+", 2);
if(parts.length<2)
{
event.reply(event.getClient().getError()+" Please include a playlist name and URLs to add!");
CommandLogContext.setError("missing_append_args");
return;
}
String pname = parts[0];
Playlist playlist = bot.getPlaylistLoader().getPlaylist(pname);
if(playlist==null)
{
event.reply(event.getClient().getError()+" Playlist `"+pname+"` doesn't exist!");
CommandLogContext.setError("playlist_not_found");
}
else
{
StringBuilder builder = new StringBuilder();
@@ -161,10 +185,12 @@ public class PlaylistCmd extends OwnerCommand
{
bot.getPlaylistLoader().writePlaylist(pname, builder.toString());
event.reply(event.getClient().getSuccess()+" Successfully added "+urls.length+" items to playlist `"+pname+"`!");
CommandLogContext.setMeta(new JSONObject().put("playlist_name", pname).put("added_count", urls.length));
}
catch(IOException e)
{
event.reply(event.getClient().getError()+" I was unable to append to the playlist: "+e.getLocalizedMessage());
CommandLogContext.setError("append_playlist_failed");
}
}
}
@@ -186,6 +212,7 @@ public class PlaylistCmd extends OwnerCommand
{
public ListCmd()
{
super(PlaylistCmd.this.bot);
this.name = "all";
this.aliases = new String[]{"available","list"};
this.help = "lists all available playlists";
@@ -193,25 +220,33 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(!bot.getPlaylistLoader().folderExists())
bot.getPlaylistLoader().createFolder();
if(!bot.getPlaylistLoader().folderExists())
{
event.reply(event.getClient().getWarning()+" Playlists folder does not exist and could not be created!");
CommandLogContext.setError("playlist_folder_missing");
return;
}
List<String> list = bot.getPlaylistLoader().getPlaylistNames();
if(list==null)
{
event.reply(event.getClient().getError()+" Failed to load available playlists!");
CommandLogContext.setError("playlist_list_failed");
}
else if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" There are no playlists in the Playlists folder!");
CommandLogContext.setMeta(new JSONObject().put("count", 0));
}
else
{
StringBuilder builder = new StringBuilder(event.getClient().getSuccess()+" Available playlists:\n");
list.forEach(str -> builder.append("`").append(str).append("` "));
event.reply(builder.toString());
CommandLogContext.setMeta(new JSONObject().put("count", list.size()));
}
}
}
@@ -20,8 +20,10 @@ import java.io.InputStream;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import net.dv8tion.jda.api.entities.Icon;
import org.json.JSONObject;
/**
*
@@ -31,6 +33,7 @@ public class SetavatarCmd extends OwnerCommand
{
public SetavatarCmd(Bot bot)
{
super(bot);
this.name = "setavatar";
this.help = "sets the avatar of the bot";
this.arguments = "<url>";
@@ -39,8 +42,9 @@ public class SetavatarCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
String url;
if(event.getArgs().isEmpty())
if(!event.getMessage().getAttachments().isEmpty() && event.getMessage().getAttachments().get(0).isImage())
@@ -53,16 +57,33 @@ public class SetavatarCmd extends OwnerCommand
if(s==null)
{
event.reply(event.getClient().getError()+" Invalid or missing URL");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "invalid_avatar_url"));
}
else
{
try {
event.getSelfUser().getManager().setAvatar(Icon.from(s)).queue(
v -> event.reply(event.getClient().getSuccess()+" Successfully changed avatar."),
t -> event.reply(event.getClient().getError()+" Failed to set avatar."));
v -> {
event.reply(event.getClient().getSuccess()+" Successfully changed avatar.");
logCommandEvent(event, "OK", new JSONObject()
.put("source", event.getArgs().isEmpty() ? "attachment" : "url"));
},
t -> {
event.reply(event.getClient().getError()+" Failed to set avatar.");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "avatar_set_failed"));
});
} catch(IOException e) {
event.reply(event.getClient().getError()+" Could not load from provided URL.");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "avatar_load_failed"));
}
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.entities.Activity;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SetgameCmd extends OwnerCommand
{
public SetgameCmd(Bot bot)
{
super(bot);
this.name = "setgame";
this.help = "sets the game the bot is playing";
this.arguments = "[action] [game]";
@@ -41,7 +44,7 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String title = event.getArgs().toLowerCase().startsWith("playing") ? event.getArgs().substring(7).trim() : event.getArgs();
try
@@ -49,10 +52,14 @@ public class SetgameCmd extends OwnerCommand
event.getJDA().getPresence().setActivity(title.isEmpty() ? null : Activity.playing(title));
event.reply(event.getClient().getSuccess()+" **"+event.getSelfUser().getName()
+"** is "+(title.isEmpty() ? "no longer playing anything." : "now playing `"+title+"`"));
CommandLogContext.setMeta(new JSONObject()
.put("activity_type", "PLAYING")
.put("title", title.isEmpty() ? JSONObject.NULL : title));
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
@@ -60,6 +67,7 @@ public class SetgameCmd extends OwnerCommand
{
private SetstreamCmd()
{
super(SetgameCmd.this.bot);
this.name = "stream";
this.aliases = new String[]{"twitch","streaming"};
this.help = "sets the game the bot is playing to a stream";
@@ -68,12 +76,13 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String[] parts = event.getArgs().split("\\s+", 2);
if(parts.length<2)
{
event.replyError("Please include a twitch username and the name of the game to 'stream'");
CommandLogContext.setError("missing_stream_args");
return;
}
try
@@ -81,10 +90,15 @@ public class SetgameCmd extends OwnerCommand
event.getJDA().getPresence().setActivity(Activity.streaming(parts[1], "https://twitch.tv/"+parts[0]));
event.replySuccess("**"+event.getSelfUser().getName()
+"** is now streaming `"+parts[1]+"`");
CommandLogContext.setMeta(new JSONObject()
.put("activity_type", "STREAMING")
.put("title", parts[1])
.put("user", parts[0]));
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
}
@@ -93,6 +107,7 @@ public class SetgameCmd extends OwnerCommand
{
private SetlistenCmd()
{
super(SetgameCmd.this.bot);
this.name = "listen";
this.aliases = new String[]{"listening"};
this.help = "sets the game the bot is listening to";
@@ -101,11 +116,12 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.replyError("Please include a title to listen to!");
CommandLogContext.setError("missing_listen_title");
return;
}
String title = event.getArgs().toLowerCase().startsWith("to") ? event.getArgs().substring(2).trim() : event.getArgs();
@@ -113,8 +129,10 @@ public class SetgameCmd extends OwnerCommand
{
event.getJDA().getPresence().setActivity(Activity.listening(title));
event.replySuccess("**"+event.getSelfUser().getName()+"** is now listening to `"+title+"`");
CommandLogContext.setMeta(new JSONObject().put("activity_type", "LISTENING").put("title", title));
} catch(Exception e) {
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
}
@@ -123,6 +141,7 @@ public class SetgameCmd extends OwnerCommand
{
private SetwatchCmd()
{
super(SetgameCmd.this.bot);
this.name = "watch";
this.aliases = new String[]{"watching"};
this.help = "sets the game the bot is watching";
@@ -131,11 +150,12 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.replyError("Please include a title to watch!");
CommandLogContext.setError("missing_watch_title");
return;
}
String title = event.getArgs();
@@ -143,8 +163,10 @@ public class SetgameCmd extends OwnerCommand
{
event.getJDA().getPresence().setActivity(Activity.watching(title));
event.replySuccess("**"+event.getSelfUser().getName()+"** is now watching `"+title+"`");
CommandLogContext.setMeta(new JSONObject().put("activity_type", "WATCHING").put("title", title));
} catch(Exception e) {
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
}
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.exceptions.RateLimitedException;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SetnameCmd extends OwnerCommand
{
public SetnameCmd(Bot bot)
{
super(bot);
this.name = "setname";
this.help = "sets the name of the bot";
this.arguments = "<name>";
@@ -36,21 +39,24 @@ public class SetnameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
try
{
String oldname = event.getSelfUser().getName();
event.getSelfUser().getManager().setName(event.getArgs()).complete(false);
event.reply(event.getClient().getSuccess()+" Name changed from `"+oldname+"` to `"+event.getArgs()+"`");
CommandLogContext.setMeta(new JSONObject().put("before", oldname).put("after", event.getArgs()));
}
catch(RateLimitedException e)
{
event.reply(event.getClient().getError()+" Name can only be changed twice per hour!");
CommandLogContext.setError("rate_limited");
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" That name is not valid!");
CommandLogContext.setError("invalid_name");
}
}
}
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.OnlineStatus;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SetstatusCmd extends OwnerCommand
{
public SetstatusCmd(Bot bot)
{
super(bot);
this.name = "setstatus";
this.help = "sets the status the bot displays";
this.arguments = "<status>";
@@ -36,21 +39,27 @@ public class SetstatusCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
try {
OnlineStatus status = OnlineStatus.fromKey(event.getArgs());
if(status==OnlineStatus.UNKNOWN)
{
event.replyError("Please include one of the following statuses: `ONLINE`, `IDLE`, `DND`, `INVISIBLE`");
CommandLogContext.setError("invalid_status");
}
else
{
OnlineStatus before = event.getJDA().getPresence().getStatus();
event.getJDA().getPresence().setStatus(status);
event.replySuccess("Set the status to `"+status.getKey().toUpperCase()+"`");
CommandLogContext.setMeta(new JSONObject()
.put("before", before == null ? JSONObject.NULL : before.getKey())
.put("after", status.getKey()));
}
} catch(Exception e) {
event.reply(event.getClient().getError()+" The status could not be set!");
CommandLogContext.setError("set_status_failed");
}
}
}
@@ -18,6 +18,8 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -25,11 +27,9 @@ import com.jagrosh.jmusicbot.commands.OwnerCommand;
*/
public class ShutdownCmd extends OwnerCommand
{
private final Bot bot;
public ShutdownCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.name = "shutdown";
this.help = "safely shuts down";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -37,8 +37,9 @@ public class ShutdownCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
CommandLogContext.setMeta(new JSONObject().put("invoked", true));
event.replyWarning("Shutting down...");
bot.shutdown();
}
@@ -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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.datalog;
import org.json.JSONObject;
public final class CommandLogContext
{
private static final ThreadLocal<CommandLogEntry> CONTEXT = new ThreadLocal<>();
private CommandLogContext()
{
}
public static void clear()
{
CONTEXT.remove();
}
public static void setMeta(JSONObject meta)
{
getOrCreate().meta = meta;
}
public static void setResult(String result)
{
getOrCreate().result = result;
}
public static void setError(String reason)
{
CommandLogEntry entry = getOrCreate();
entry.result = "ERROR";
if(reason != null && !reason.isEmpty())
{
if(entry.meta == null)
entry.meta = new JSONObject();
entry.meta.put("error_reason", reason);
}
}
public static void setManualLogging()
{
getOrCreate().manual = true;
}
public static CommandLogEntry consume()
{
CommandLogEntry entry = CONTEXT.get();
CONTEXT.remove();
return entry;
}
private static CommandLogEntry getOrCreate()
{
CommandLogEntry entry = CONTEXT.get();
if(entry == null)
{
entry = new CommandLogEntry();
CONTEXT.set(entry);
}
return entry;
}
public static final class CommandLogEntry
{
public JSONObject meta;
public String result;
public boolean manual;
}
}
File diff suppressed because it is too large. Load diff
@@ -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;
}
}
@@ -53,7 +53,7 @@ public class PlaylistLoader
else
{
createFolder();
return Collections.EMPTY_LIST;
return Collections.emptyList();
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2022 John Grosh (jagrosh).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.queue;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
*
* @author Wolfgang Schwendtbauer
* @param <T>
*/
public abstract class AbstractQueue<T extends Queueable>
{
protected AbstractQueue(AbstractQueue<T> queue)
{
this.list = queue != null ? queue.getList() : new LinkedList<>();
}
protected final List<T> list;
public abstract int add(T item);
public void addAt(int index, T item)
{
if(index >= list.size())
list.add(item);
else
list.add(index, item);
}
public int size() {
return list.size();
}
public T pull() {
return list.remove(0);
}
public boolean isEmpty()
{
return list.isEmpty();
}
public List<T> getList()
{
return list;
}
public T get(int index) {
return list.get(index);
}
public T remove(int index)
{
return list.remove(index);
}
public int removeAll(long identifier)
{
int count = 0;
for(int i=list.size()-1; i>=0; i--)
{
if(list.get(i).getIdentifier()==identifier)
{
list.remove(i);
count++;
}
}
return count;
}
public void clear()
{
list.clear();
}
public int shuffle(long identifier)
{
List<Integer> iset = new ArrayList<>();
for(int i=0; i<list.size(); i++)
{
if(list.get(i).getIdentifier()==identifier)
iset.add(i);
}
for(int j=0; j<iset.size(); j++)
{
int first = iset.get(j);
int second = iset.get((int)(Math.random()*iset.size()));
T temp = list.get(first);
list.set(first, list.get(second));
list.set(second, temp);
}
return iset.size();
}
public void skip(int number)
{
if (number > 0) {
list.subList(0, number).clear();
}
}
/**
* Move an item to a different position in the list
* @param from The position of the item
* @param to The new position of the item
* @return the moved item
*/
public T moveItem(int from, int to)
{
T item = list.remove(from);
list.add(to, item);
return item;
}
}
@@ -15,9 +15,7 @@
*/
package com.jagrosh.jmusicbot.queue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
@@ -25,15 +23,21 @@ import java.util.Set;
* @author John Grosh (jagrosh)
* @param <T>
*/
public class FairQueue<T extends Queueable> {
private final List<T> list = new ArrayList<>();
private final Set<Long> set = new HashSet<>();
public class FairQueue<T extends Queueable> extends AbstractQueue<T>
{
public FairQueue(AbstractQueue<T> queue)
{
super(queue);
}
protected final Set<Long> set = new HashSet<>();
@Override
public int add(T item)
{
int lastIndex;
for(lastIndex=list.size()-1; lastIndex>-1; lastIndex--)
if(list.get(lastIndex).getIdentifier()==item.getIdentifier())
if(list.get(lastIndex).getIdentifier() == item.getIdentifier())
break;
lastIndex++;
set.clear();
@@ -46,99 +50,5 @@ public class FairQueue<T extends Queueable> {
list.add(lastIndex, item);
return lastIndex;
}
public void addAt(int index, T item)
{
if(index >= list.size())
list.add(item);
else
list.add(index, item);
}
public int size()
{
return list.size();
}
public T pull()
{
return list.remove(0);
}
public boolean isEmpty()
{
return list.isEmpty();
}
public List<T> getList()
{
return list;
}
public T get(int index)
{
return list.get(index);
}
public T remove(int index)
{
return list.remove(index);
}
public int removeAll(long identifier)
{
int count = 0;
for(int i=list.size()-1; i>=0; i--)
{
if(list.get(i).getIdentifier()==identifier)
{
list.remove(i);
count++;
}
}
return count;
}
public void clear()
{
list.clear();
}
public int shuffle(long identifier)
{
List<Integer> iset = new ArrayList<>();
for(int i=0; i<list.size(); i++)
{
if(list.get(i).getIdentifier()==identifier)
iset.add(i);
}
for(int j=0; j<iset.size(); j++)
{
int first = iset.get(j);
int second = iset.get((int)(Math.random()*iset.size()));
T temp = list.get(first);
list.set(first, list.get(second));
list.set(second, temp);
}
return iset.size();
}
public void skip(int number)
{
for(int i=0; i<number; i++)
list.remove(0);
}
/**
* Move an item to a different position in the list
* @param from The position of the item
* @param to The new position of the item
* @return the moved item
*/
public T moveItem(int from, int to)
{
T item = list.remove(from);
list.add(to, item);
return item;
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2022 John Grosh (jagrosh).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.queue;
/**
*
* @author Wolfgang Schwendtbauer
* @param <T>
*/
public class LinearQueue<T extends Queueable> extends AbstractQueue<T>
{
public LinearQueue(AbstractQueue<T> queue)
{
super(queue);
}
@Override
public int add(T item)
{
list.add(item);
return list.size() - 1;
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2022 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.queue;
/**
*
* @author Wolfgang Schwendtbauer
*/
@FunctionalInterface
public interface QueueSupplier
{
<T extends Queueable> AbstractQueue<T> apply(AbstractQueue<T> queue);
}
@@ -0,0 +1,69 @@
/*
* Copyright 2022 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.settings;
import com.jagrosh.jmusicbot.queue.AbstractQueue;
import com.jagrosh.jmusicbot.queue.FairQueue;
import com.jagrosh.jmusicbot.queue.LinearQueue;
import com.jagrosh.jmusicbot.queue.Queueable;
import com.jagrosh.jmusicbot.queue.QueueSupplier;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* @author Wolfgang Schwendtbauer
*/
public enum QueueType
{
LINEAR("\u23E9", "Linear", LinearQueue::new), // ⏩
FAIR("\uD83D\uDD22", "Fair", FairQueue::new); // 🔢
private final String userFriendlyName;
private final String emoji;
private final QueueSupplier supplier;
QueueType(final String emoji, final String userFriendlyName, QueueSupplier supplier)
{
this.userFriendlyName = userFriendlyName;
this.emoji = emoji;
this.supplier = supplier;
}
public static List<String> getNames()
{
return Arrays.stream(QueueType.values())
.map(type -> type.name().toLowerCase())
.collect(Collectors.toList());
}
public <T extends Queueable> AbstractQueue<T> createInstance(AbstractQueue<T> previous)
{
return supplier.apply(previous);
}
public String getUserFriendlyName()
{
return userFriendlyName;
}
public String getEmoji()
{
return emoji;
}
}
@@ -20,8 +20,8 @@ import java.util.Collection;
import java.util.Collections;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
*
@@ -36,10 +36,18 @@ public class Settings implements GuildSettingsProvider
private int volume;
private String defaultPlaylist;
private RepeatMode repeatMode;
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)
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
@@ -71,9 +79,24 @@ public class Settings implements GuildSettingsProvider
this.repeatMode = repeatMode;
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)
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;
@@ -84,6 +107,9 @@ public class Settings implements GuildSettingsProvider
this.repeatMode = repeatMode;
this.prefix = prefix;
this.skipRatio = skipRatio;
this.queueType = queueType;
this.statsReportChannelId = statsReportChannelId;
this.statsReportFrequency = statsReportFrequency == null ? StatsReportFrequency.OFF : statsReportFrequency;
}
// Getters
@@ -127,10 +153,30 @@ public class Settings implements GuildSettingsProvider
return skipRatio;
}
public QueueType getQueueType()
{
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()
{
return prefix == null ? Collections.EMPTY_SET : Collections.singleton(prefix);
return prefix == null ? Collections.emptySet() : Collections.singleton(prefix);
}
// Setters
@@ -181,4 +227,22 @@ public class Settings implements GuildSettingsProvider
this.skipRatio = skipRatio;
this.manager.writeSettings();
}
public void setQueueType(QueueType queueType)
{
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();
}
}
@@ -19,26 +19,30 @@ import com.jagrosh.jdautilities.command.GuildSettingsManager;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.HashMap;
import net.dv8tion.jda.api.entities.Guild;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class SettingsManager implements GuildSettingsManager
public class SettingsManager implements GuildSettingsManager<Settings>
{
private final static double SKIP_RATIO = .55;
private final static Logger LOG = LoggerFactory.getLogger("Settings");
private final static String SETTINGS_FILE = "serversettings.json";
private final HashMap<Long,Settings> settings;
public SettingsManager()
{
this.settings = new HashMap<>();
try {
JSONObject loadedSettings = new JSONObject(new String(Files.readAllBytes(OtherUtil.getPath("serversettings.json"))));
JSONObject loadedSettings = new JSONObject(new String(Files.readAllBytes(OtherUtil.getPath(SETTINGS_FILE))));
loadedSettings.keySet().forEach((id) -> {
JSONObject o = loadedSettings.getJSONObject(id);
@@ -55,16 +59,30 @@ public class SettingsManager implements GuildSettingsManager
o.has("default_playlist")? o.getString("default_playlist") : null,
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") : SKIP_RATIO));
o.has("skip_ratio") ? o.getDouble("skip_ratio") : -1,
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
try {
LOG.info("serversettings.json will be created in " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
Files.write(OtherUtil.getPath("serversettings.json"), new JSONObject().toString(4).getBytes());
} catch(IOException ex) {
LOG.warn("Failed to create new settings file: "+ex);
}
return;
} catch(IOException | JSONException e) {
LoggerFactory.getLogger("Settings").warn("Failed to load server settings (this is normal if no settings have been set yet): "+e);
LOG.warn("Failed to load server settings: "+e);
}
LOG.info("serversettings.json loaded from " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
}
/**
* Gets non-null settings for a Guild
*
*
* @param guild the guild to get settings for
* @return the existing settings, or new settings for that guild
*/
@@ -73,17 +91,17 @@ public class SettingsManager implements GuildSettingsManager
{
return getSettings(guild.getIdLong());
}
public Settings getSettings(long guildId)
{
return settings.computeIfAbsent(guildId, id -> createDefaultSettings());
}
private Settings createDefaultSettings()
{
return new Settings(this, 0, 0, 0, 100, null, RepeatMode.OFF, null, SKIP_RATIO);
return new Settings(this, 0, 0, 0, 100, null, RepeatMode.OFF, null, -1, QueueType.FAIR, 0L, StatsReportFrequency.OFF);
}
protected void writeSettings()
{
JSONObject obj = new JSONObject();
@@ -104,14 +122,20 @@ public class SettingsManager implements GuildSettingsManager
o.put("repeat_mode", s.getRepeatMode());
if(s.getPrefix() != null)
o.put("prefix", s.getPrefix());
if(s.getSkipRatio() != SKIP_RATIO)
if(s.getSkipRatio() != -1)
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 {
Files.write(OtherUtil.getPath("serversettings.json"), obj.toString(4).getBytes());
Files.write(OtherUtil.getPath(SETTINGS_FILE), obj.toString(4).getBytes());
} catch(IOException ex){
LoggerFactory.getLogger("Settings").warn("Failed to write to file: "+ex);
LOG.warn("Failed to write to file: "+ex);
}
}
}
@@ -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,29 +15,41 @@
*/
package com.jagrosh.jmusicbot.utils;
import com.jagrosh.jmusicbot.audio.RequestMetadata.UserInfo;
import java.util.List;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
*
* @author John Grosh <john.a.grosh@gmail.com>
*/
public class FormatUtil {
public static String formatTime(long duration)
public static String formatUsername(String username, String discrim)
{
if(duration == Long.MAX_VALUE)
return "LIVE";
long seconds = Math.round(duration/1000.0);
long hours = seconds/(60*60);
seconds %= 60*60;
long minutes = seconds/60;
seconds %= 60;
return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds);
if(discrim == null || discrim.equals("0000"))
{
return username;
}
else
{
return username + "#" + discrim;
}
}
public static String formatUsername(UserInfo userinfo)
{
return formatUsername(userinfo.username, userinfo.discrim);
}
public static String formatUsername(User user)
{
return formatUsername(user.getName(), user.getDiscriminator());
}
public static String progressBar(double percent)
{
String str = "";
@@ -82,7 +94,7 @@ public class FormatUtil {
public static String listOfRoles(List<Role> list, String query)
{
String out = " Multiple text channels found matching \""+query+"\":";
String out = " Multiple roles found matching \""+query+"\":";
for(int i=0; i<6 && i<list.size(); i++)
out+="\n - "+list.get(i).getName()+" (ID:"+list.get(i).getId()+")";
if(list.size()>6)
@@ -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,8 +24,13 @@ 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;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.ApplicationInfo;
import net.dv8tion.jda.api.entities.User;
import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;
@@ -39,7 +45,11 @@ public class OtherUtil
public final static String NEW_VERSION_AVAILABLE = "There is a new version of JMusicBot available!\n"
+ "Current version: %s\n"
+ "New Version: %s\n\n"
+ "Please visit https://github.com/jagrosh/MusicBot/releases/latest to get the latest release.";
+ "Please visit https://github.com/chrisb09/MusicBot/releases to get the latest release."
+ "If you are using docker (or docker-compose), please pull the new image with:"
+ "Docker: docker pull chrisb09/jmusicbot"
+ "Docker-compose: docker-compose pull"+
" and don't forget to restart the container.";
private final static String WINDOWS_INVALID_PATH = "c:\\windows\\system32\\";
/**
@@ -60,7 +70,7 @@ public class OtherUtil
{
result = Paths.get(new File(JMusicBot.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getPath() + File.separator + path);
}
catch(URISyntaxException ex) {}
catch(URISyntaxException ignored) {}
}
return result;
}
@@ -80,7 +90,7 @@ public class OtherUtil
reader.lines().forEach(line -> sb.append("\r\n").append(line));
return sb.toString().trim();
}
catch(IOException ex)
catch(IOException ignored)
{
return null;
}
@@ -150,7 +160,14 @@ public class OtherUtil
return st == null ? OnlineStatus.ONLINE : st;
}
public static String checkVersion(Prompt prompt)
public static void checkJavaVersion(Prompt prompt)
{
if(!System.getProperty("java.vm.name").contains("64"))
prompt.alert(Prompt.Level.WARNING, "Java Version",
"It appears that you may not be using a supported Java version. Please use 64-bit java.");
}
public static void checkVersion(Prompt prompt)
{
// Get current version number
String version = getCurrentVersion();
@@ -160,11 +177,8 @@ public class OtherUtil
if(latestVersion!=null && !latestVersion.equals(version))
{
prompt.alert(Prompt.Level.WARNING, "Version", String.format(NEW_VERSION_AVAILABLE, version, latestVersion));
prompt.alert(Prompt.Level.WARNING, "JMusicBot Version", String.format(NEW_VERSION_AVAILABLE, version, latestVersion));
}
// Return the current version
return version;
}
public static String getCurrentVersion()
@@ -174,13 +188,33 @@ 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()
{
try
{
Response response = new OkHttpClient.Builder().build()
.newCall(new Request.Builder().get().url("https://api.github.com/repos/jagrosh/MusicBot/releases/latest").build())
.newCall(new Request.Builder().get().url("https://api.github.com/repos/chrisb09/MusicBot/releases/latest").build())
.execute();
ResponseBody body = response.body();
if(body != null)
@@ -203,4 +237,23 @@ public class OtherUtil
return null;
}
}
/**
* Checks if the bot JMusicBot is being run on is supported & returns the reason if it is not.
* @return A string with the reason, or null if it is supported.
*/
public static String getUnsupportedBotReason(JDA jda)
{
if (jda.getSelfUser().getFlags().contains(User.UserFlag.VERIFIED_BOT))
return "The bot is verified. Using JMusicBot in a verified bot is not supported.";
ApplicationInfo info = jda.retrieveApplicationInfo().complete();
if (info.isBotPublic())
return "\"Public Bot\" is enabled. Using JMusicBot as a public bot is not supported. Please disable it in the "
+ "Developer Dashboard at https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot ."
+ "You may also need to disable all Installation Contexts at https://discord.com/developers/applications/"
+ jda.getSelfUser().getId() + "/installation .";
return null;
}
}
@@ -0,0 +1,138 @@
/*
* Copyright 2020 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.utils;
public class TimeUtil
{
public static String formatTime(long duration)
{
if(duration == Long.MAX_VALUE)
return "LIVE";
long seconds = Math.round(duration/1000.0);
long hours = seconds/(60*60);
seconds %= 60*60;
long minutes = seconds/60;
seconds %= 60;
return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds);
}
/**
* Parses a seek time string into milliseconds and determines if it's relative.
* Supports "colon time" (HH:MM:SS) or "unit time" (1h20m)
* @param args time string
* @return SeekTime object, or null if the string could not be parsed
*/
public static SeekTime parseTime(String args)
{
if (args.length() == 0) return null;
String timestamp = args;
boolean relative = false; // seek forward or backward
boolean isSeekingBackwards = false;
char first = timestamp.charAt(0);
if (first == '+' || first == '-')
{
relative = true;
isSeekingBackwards = first == '-';
timestamp = timestamp.substring(1);
}
long milliseconds = parseColonTime(timestamp);
if(milliseconds == -1) milliseconds = parseUnitTime(timestamp);
if(milliseconds == -1) return null;
milliseconds *= isSeekingBackwards ? -1 : 1;
return new SeekTime(milliseconds, relative);
}
/**
* @param timestamp timestamp formatted as: [+ | -] &lt;HH:MM:SS | MM:SS | SS&gt;
* @return Time in milliseconds
*/
public static long parseColonTime(String timestamp)
{
String[] timestampSplitArray = timestamp.split(":+");
if(timestampSplitArray.length > 3 )
return -1;
double[] timeUnitArray = new double[3]; // hours, minutes, seconds
for(int index = 0; index < timestampSplitArray.length; index++)
{
String unit = timestampSplitArray[index];
if (unit.startsWith("+") || unit.startsWith("-")) return -1;
unit = unit.replace(",", ".");
try
{
timeUnitArray[index + 3 - timestampSplitArray.length] = Double.parseDouble(unit);
}
catch (NumberFormatException e)
{
return -1;
}
}
return Math.round(timeUnitArray[0] * 3600000 + timeUnitArray[1] * 60000 + timeUnitArray[2] * 1000);
}
/**
*
* @param timestr time string formatted as a unit time, e.g. 20m10, 1d5h20m14s or 1h and 20m
* @return Time in milliseconds
*/
public static long parseUnitTime(String timestr)
{
timestr = timestr.replaceAll("(?i)(\\s|,|and)","")
.replaceAll("(?is)(-?\\d+|[a-z]+)", "$1 ")
.trim();
String[] vals = timestr.split("\\s+");
int time = 0;
try
{
for(int j=0; j<vals.length; j+=2)
{
int num = Integer.parseInt(vals[j]);
if(vals.length > j+1)
{
if(vals[j+1].toLowerCase().startsWith("m"))
num*=60;
else if(vals[j+1].toLowerCase().startsWith("h"))
num*=60*60;
else if(vals[j+1].toLowerCase().startsWith("d"))
num*=60*60*24;
}
time+=num*1000;
}
}
catch(Exception ex)
{
return -1;
}
return time;
}
public static class SeekTime
{
public final long milliseconds;
public final boolean relative;
private SeekTime(long milliseconds, boolean relative)
{
this.milliseconds = milliseconds;
this.relative = relative;
}
}
}
@@ -0,0 +1,193 @@
package com.jagrosh.jmusicbot.utils;
import com.sedmelluq.lava.extensions.youtuberotator.planner.*;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
import dev.lavalink.youtube.clients.Web;
import org.json.JSONObject;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.chromium.ChromiumDriverLogLevel;
import org.openqa.selenium.devtools.NetworkInterceptor;
import org.openqa.selenium.os.ExecutableFinder;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.Filter;
import org.openqa.selenium.remote.http.HttpMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.github.bonigarcia.wdm.WebDriverManager;
public class YouTubeUtil {
private final static Logger LOGGER = LoggerFactory.getLogger(YouTubeUtil.class);
public enum RoutingPlanner {
NONE,
ROTATE_ON_BAN,
LOAD_BALANCE,
NANO_SWITCH,
ROTATING_NANO_SWITCH
}
public static IpBlock parseIpBlock(String cidr) {
if (Ipv6Block.isIpv6CidrBlock(cidr))
return new Ipv6Block(cidr);
if (Ipv4Block.isIpv4CidrBlock(cidr))
return new Ipv4Block(cidr);
throw new IllegalArgumentException("Could not parse CIDR " + cidr);
}
public static AbstractRoutePlanner createRouterPlanner(RoutingPlanner routingPlanner, List<IpBlock> ipBlocks) {
switch (routingPlanner) {
case NONE:
return null;
case ROTATE_ON_BAN:
return new RotatingIpRoutePlanner(ipBlocks);
case LOAD_BALANCE:
return new BalancingIpRoutePlanner(ipBlocks);
case NANO_SWITCH:
return new NanoIpRoutePlanner(ipBlocks, true);
case ROTATING_NANO_SWITCH:
return new RotatingNanoIpRoutePlanner(ipBlocks);
default:
throw new IllegalArgumentException("Unknown RoutingPlanner value provided");
}
}
public static void GetPoTokenAndVisitorData(String userChromePath, String userChromeDriverPath, boolean headless)
{
//
// 1. Find a Chrome install & launch the WebDriver
//
WebDriverManager wdm = WebDriverManager.chromedriver();
if (userChromePath != null) {
wdm.browserVersionDetectionCommand(userChromePath + " --version");
}
else if (wdm.getBrowserPath().isEmpty())
{
// try finding a Chromium browser instead (Linux users usually install Chromium instead of Google Chrome)
wdm = WebDriverManager.chromiumdriver();
if (wdm.getBrowserPath().isEmpty()) {
LOGGER.error("Could not obtain a PO token for YouTube playback, because no Chrome browser could be found. Please install Google Chrome or Chromium!");
return;
}
}
if (userChromeDriverPath == null)
// Automatically download & set up ChromeDriver for the Chrome/Chromium version that the user has installed
wdm.setup();
else {
// Find & use the ChromeDriver executable the user provided
String path = new ExecutableFinder().find(userChromeDriverPath);
if (path == null) {
LOGGER.error("Could not obtain a PO token for YouTube playback, because the specified ChromeDriver could not be found. " +
"Please check your config's value of the chromedriverpath, or set it to \"AUTO\" to have JMusicBot automatically download ChromeDriver");
return;
}
LOGGER.info("Using ChromeDriver at {}", path);
System.setProperty("webdriver.chrome.driver", path);
}
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setBinary(userChromePath != null ? userChromePath : wdm.getBrowserPath().get().toString());
if (!headless)
chromeOptions.addArguments("--auto-open-devtools-for-tabs");
else
chromeOptions.addArguments("--headless=new");
ChromeDriverService.Builder chromeDriverBuilder = new ChromeDriverService.Builder();
if (LOGGER.isDebugEnabled())
chromeDriverBuilder.withLogLevel(ChromiumDriverLogLevel.DEBUG);
WebDriver driver = new ChromeDriver(chromeDriverBuilder.build(), chromeOptions);
//
// 2. Setup a network interceptor to intercept the player API request & obtain po token and visitor data
//
// as the network interceptor below runs on a different thread, we'll have it notify us on this/current thread
// whether retrieving the data failed (completes with the Exception) or succeeded (completes with null)
CompletableFuture<Exception> interceptorFuture = new CompletableFuture<>();
NetworkInterceptor interceptor = new NetworkInterceptor(driver,
(Filter) next -> req -> {
if (req.getMethod() != HttpMethod.POST || !req.getUri().contains("/youtubei/v1/player"))
return next.execute(req);
try {
String rawBody = Contents.string(req);
JSONObject body = new JSONObject(rawBody);
String visitorData = body
.getJSONObject("context")
.getJSONObject("client")
.getString("visitorData");
String poToken = body
.getJSONObject("serviceIntegrityDimensions")
.getString("poToken");
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
LOGGER.debug("PO token: {}", poToken);
LOGGER.debug("Visitor Data: {}", visitorData);
if (poToken.length() < 160)
LOGGER.warn("There is a high chance that the retrieved PO token will not work. Try on a different network.");
Web.setPoTokenAndVisitorData(poToken, visitorData);
interceptorFuture.complete(null);
} catch (Exception e) {
interceptorFuture.complete(e);
}
return next.execute(req);
});
//
// 3. Navigate to YouTube & start the player
//
try {
driver.get("https://www.youtube.com/embed/aqz-KE-bpKQ");
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
WebElement moviePlayer = driver.findElement(By.cssSelector("#movie_player"));
moviePlayer.click();
try {
// Wait for the network interceptor to tell us if it got the data
Exception result = interceptorFuture.get(20, TimeUnit.SECONDS);
if (result != null)
throw new Exception("Failed to read YouTube player request body", result);
} catch (TimeoutException e) {
throw new Exception("Timed out waiting for YouTube player request");
}
} catch (Exception e) {
LOGGER.error("Failed to obtain PO token for YouTube playback: {}", e.getMessage());
LOGGER.debug("Exception:", e);
} finally {
interceptor.close();
// Only quit the browser when not in headless mode. Helps with troubleshooting.
if (headless)
driver.quit();
}
}
}
@@ -0,0 +1,91 @@
package com.jagrosh.jmusicbot.utils;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import java.nio.file.Files;
/**
* A logback turbo filter, used retrieve the YouTube OAuth2 refresh token that gets logged once authorized with YouTube.
*
* @author Michaili K. <git@michaili.dev>
*/
public class YoutubeOauth2TokenHandler extends TurboFilter {
public final static org.slf4j.Logger LOGGER = LoggerFactory.getLogger(YoutubeOauth2TokenHandler.class);
private Data data;
public void init()
{
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.addTurboFilter(this);
}
public Data getData()
{
return data;
}
@Override
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t)
{
if (!logger.getName().equals("dev.lavalink.youtube.http.YoutubeOauth2Handler"))
return FilterReply.NEUTRAL;
if (format.equals("OAUTH INTEGRATION: To give youtube-source access to your account, go to {} and enter code {}"))
{
this.data = new Data((String) params[0], (String) params[1]);
return FilterReply.NEUTRAL;
}
if (format.equals("OAUTH INTEGRATION: Token retrieved successfully. Store your refresh token as this can be reused. ({})"))
{
LOGGER.info(
"Authorization successful & retrieved token! Storing the token in {}",
OtherUtil.getPath("youtubetoken.txt").toAbsolutePath()
);
try
{
Files.write(OtherUtil.getPath("youtubetoken.txt"), params[0].toString().getBytes());
}
catch (Exception e)
{
LOGGER.error(
"Failed to write the YouTube OAuth2 refresh token to storage! You will need to authorize again on the next reboot",
e
);
}
return FilterReply.DENY;
}
return FilterReply.NEUTRAL;
}
public static class Data
{
private final String authorisationUrl;
private final String code;
private Data(String authorisationUrl, String code)
{
this.authorisationUrl = authorisationUrl;
this.code = code;
}
public String getCode()
{
return code;
}
public String getAuthorisationUrl()
{
return authorisationUrl;
}
}
}
Binary file not shown.
Binary file not shown.
+95
View File
@@ -48,6 +48,14 @@ game = "DEFAULT"
status = ONLINE
// If you set this to true, the bot will use a Google account to play YouTube tracks.
// This is only needed if you are experiencing issues with YouTube playback.
// Once enabled, instructions for login will be written to the console and DMs.
// DO NOT use your main Google account! Use an alternative account.
youtubeoauth2=false
// If you set this to true, the bot will list the title of the song it is currently playing in its
// "Playing" status. Note that this will ONLY work if the bot is playing music on ONE guild;
// if the bot is playing on multiple guilds, this will not work.
@@ -55,6 +63,13 @@ status = ONLINE
songinstatus=false
// If you set this to true, the bot will maintain a single status message in the text channel
// showing the currently playing song and queue summary. Set this to false to disable
// the status message feature entirely.
statusmessages=true
// If you set this, the bot will also use this prefix in addition to
// the one provided above
@@ -98,6 +113,22 @@ stayinchannel = false
maxtime = 0
// This sets the maximum number of pages of songs that can be loaded from a YouTube
// playlist. Each page can contain up to 100 tracks. Playing a playlist with more
// pages than the maximum will stop loading after the provided number of pages.
// For example, if the max was set to 15 and a playlist contained 1850 tracks,
// only the first 1500 tracks (15 pages) would be loaded. By default, this is
// set to 10 pages (1000 tracks).
maxytplaylistpages = 10
// This sets the ratio of users that must vote to skip the currently playing song.
// Guild owners can define their own skip ratios, but this will be used if a guild
// has not defined their own skip ratio.
skipratio = 0.55
// This sets the amount of seconds the bot will stay alone on a voice channel until it
// automatically leaves the voice channel and clears the queue. If not set or set
@@ -167,6 +198,7 @@ aliases {
movetrack = [ move ]
pause = []
playnext = []
queuetype = []
repeat = []
skipto = [ jumpto ]
stop = [ leave ]
@@ -174,6 +206,68 @@ aliases {
}
// This sets the logging verbosity.
// Available levels: off, error, warn, info, debug, trace, all
//
// It is recommended to leave this at info. Debug log levels might help with troubleshooting,
// but can contain sensitive data.
loglevel = info
// This sets the executable path of where a Chrome or Chromium installation is located JMusicBot can use.
// JMusicBot may launch a web browser to get around anti-bot checks.
// Set this to "AUTO" to make JMusicBot attempt to find a Chrome/Chromium installation itself.
chromepath = "AUTO"
// This sets the executable path of where a ChromeDriver installation is located JMusicBot can use.
// Set this to "AUTO" to make JMusicBot automatically download the ChromeDriver version
// which matches the installed Chrome version.
chromedriverpath = "AUTO"
// This sets whether JMusicBot should launch Chrome/Chromium instances without a GUI.
// It is recommended to leave this set to true.
// If set to false, JMusicBot will not close the browser automatically (even when finished)
// and will automatically open DevTools for troubleshooting purposes.
chromeheadless = true
// Data logging (analytics) configuration
// Enable to log queue/playback events into a SQL database.
// SQLite example: jdbc:sqlite:musicbot.db
dblog.enabled = false
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
dblog.user = ""
dblog.password = ""
// This configures the routing planner to use for IP rotation on YouTube.
// When set, you need to also set an IP block below.
// The following routing strategies are available:
// NONE = Disable
// ROTATE_ON_BAN = Switch IP when currently used address gets banned.
// LOAD_BALANCE = Selects random IP for each track playback.
// NANO_SWITCH = Selects IPv6 address based on current nanosecond. Recommended for one /64 IPv6 blocks.
// ROTATING_NANO_SWITCH = Same as above, but uses the next /64 IPv6 block in the list in case of a ban.
ytroutingplanner = NONE
// The IPv4 and IPv6 blocks to use when using the above routing planner.
// This does nothing when ytroutingplanner is set to none.
// The list accepts a list of IPv4 or IPv6 CIDR blocks (not a mix of them however)
// IPv4 example: [ "192.0.2.183", "198.51.100.153/29" ]
// IPv6 example: [ "2a02:1234:5678:9abc::/64" ]
ytipblocks = [ ]
// Transforms are used to modify specific play inputs and convert them to different kinds of inputs
// These are quite complicated to use, and have limited use-cases, but in theory allow for rough
// whitelists or blacklists, roundabout loading from some sources, and customization of how things are
@@ -198,6 +292,7 @@ transforms = {}
// IF SOMEONE ASKS YOU TO ENABLE THIS, THERE IS AN 11/10 CHANCE THEY ARE TRYING TO SCAM YOU
eval=false
evalengine="Nashorn"
/// END OF JMUSICBOT CONFIG ///
@@ -29,7 +29,7 @@ public class FairQueueTest
@Test
public void differentIdentifierSize()
{
FairQueue<Q> queue = new FairQueue<>();
FairQueue<Q> queue = new FairQueue<>(null);
int size = 100;
for(int i=0; i<size; i++)
queue.add(new Q(i));
@@ -39,7 +39,7 @@ public class FairQueueTest
@Test
public void sameIdentifierSize()
{
FairQueue<Q> queue = new FairQueue<>();
FairQueue<Q> queue = new FairQueue<>(null);
int size = 100;
for(int i=0; i<size; i++)
queue.add(new Q(0));
@@ -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();
}
}
@@ -0,0 +1,119 @@
/*
* Copyright 2020 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* @author Whew., Inc.
*/
public class TimeUtilTest
{
@Test
public void singleDigit()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("5");
assertNotNull(seek);
assertEquals(5000, seek.milliseconds);
}
@Test
public void multipleDigits()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("99:9:999");
assertNotNull(seek);
assertEquals(357939000, seek.milliseconds);
seek = TimeUtil.parseTime("99h9m999s");
assertNotNull(seek);
assertEquals(357939000, seek.milliseconds);
}
@Test
public void decimalDigits()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("99.5:9.0:999.777");
assertNotNull(seek);
assertEquals(359739777, seek.milliseconds);
}
@Test
public void seeking()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("5");
assertNotNull(seek);
assertFalse(seek.relative);
assertEquals(5000, seek.milliseconds);
}
@Test
public void relativeSeekingForward()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("+5");
assertNotNull(seek);
assertTrue(seek.relative);
assertEquals(5000, seek.milliseconds);
}
@Test
public void relativeSeekingBackward()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("-5");
assertNotNull(seek);
assertTrue(seek.relative);
assertEquals(-5000, seek.milliseconds);
}
@Test
public void parseTimeArgumentLength()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("");
assertNull(seek);
}
@Test
public void timestampTotalUnits()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("1:1:1:1");
assertNull(seek);
seek = TimeUtil.parseTime("1h2m3m4s5s");
assertNotNull(seek);
assertEquals(3909000, seek.milliseconds);
}
@Test
public void relativeSymbol()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("+-1:-+1:+-1");
assertNull(seek);
}
@Test
public void timestampNumberFormat()
{
TimeUtil.SeekTime seek = TimeUtil.parseTime("1:1:a");
assertNull(seek);
seek = TimeUtil.parseTime("1a2s");
assertNotNull(seek);
assertEquals(3000, seek.milliseconds);
}
}