mirror of
https://github.com/chrisb09/MusicBot.git
synced 2026-11-06 01:12:40 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f82f1005
|
||
|
|
d0de6c5db9
|
||
|
|
a0393d1bba | ||
|
|
3886ccbe65 | ||
|
|
fddde60e23 | ||
|
|
838ff69090 | ||
|
|
66cc718d30 | ||
|
|
ef134be9c0 | ||
|
|
c25cda721a | ||
|
|
f95b50af89 | ||
|
|
1e6904d329 | ||
|
|
5ff7b2f28e | ||
|
|
28dfdcf680 | ||
|
|
c5073b8755 | ||
|
|
5501f02a97 | ||
|
|
66e207eabe | ||
|
|
db3a57d517 | ||
|
|
81fa3fed25 | ||
|
|
9484c373ff | ||
|
|
860822d520 | ||
|
|
a0c313047f | ||
|
|
db69ea6787 | ||
|
|
5dba2c355b | ||
|
|
9fd246ac25 | ||
|
|
6a15bbb0df | ||
|
|
16bc176685 | ||
|
|
50e8bff831 | ||
|
|
9c012eb169 | ||
|
|
87771df1fb | ||
|
|
5d35f5bb4c | ||
|
|
ecd8cdf77b | ||
|
|
39e5bf7786 | ||
|
|
650a9ea996 | ||
|
|
0e0c05acf9 | ||
|
|
2e52ecac3b | ||
|
|
450ee7acb8 | ||
|
|
a7b56b67ab | ||
|
|
fedb97e1c7 | ||
|
|
68f951dc63 | ||
|
|
01eee8813a | ||
|
|
3df09a4cd2 | ||
|
|
0084ba7baf | ||
|
|
35e756df9b | ||
|
|
426e28ed8c | ||
|
|
dd72baca0d | ||
|
|
ac4e09cbd9 | ||
|
|
fd01d94cf2 | ||
|
|
95f72937c8 | ||
|
|
792cb7a8b7 | ||
|
|
03ba83f06b | ||
|
|
3b3a483b17 | ||
|
|
1874252ff6 | ||
|
|
aa8e430ff1 | ||
|
|
204bb6f04a | ||
|
|
9ec21bf329 | ||
|
|
b419191bad | ||
|
|
aa89145ae8 | ||
|
|
293733f0e7 | ||
|
|
7be02b1fe2 | ||
|
|
1ac46d9ec5 | ||
|
|
026ce87440 | ||
|
|
e5f34cc900 | ||
|
|
c9a2477f86 | ||
|
|
61a18a1db7 | ||
|
|
7fb44e296b | ||
|
|
4ae9f05776 | ||
|
|
84facb8b82 | ||
|
|
070938b326 | ||
|
|
58a9e76edb | ||
|
|
96ffcd8291 | ||
|
|
844a1bb75a | ||
|
|
6094e49ab2 | ||
|
|
88ce3e1149 | ||
|
|
463bf75783 | ||
|
|
348fc3ceec | ||
|
|
30eafb9239 | ||
|
|
c95f2248f3 | ||
|
|
b80b0c8459 | ||
|
|
4a8392fdb9 | ||
|
|
9d6584525c | ||
|
|
909b655f5c | ||
|
|
859e5c5862 | ||
|
|
43b63f1ba8 | ||
|
|
eca2563abf | ||
|
|
e6bfd1841f | ||
|
|
f140b41bf5 | ||
|
|
8733899aa0 | ||
|
|
e5fd05e601 | ||
|
|
6a5a9c7a75 | ||
|
|
8557f7ab8e | ||
|
|
48e62f1a50 | ||
|
|
6f12c33781 | ||
|
|
0afb3db269 | ||
|
|
81322ef993 | ||
|
|
b3ffbdba7d | ||
|
|
801b3769a1 | ||
|
|
49c3ec7960 | ||
|
|
6752695b43 | ||
|
|
bb17c984a2 | ||
|
|
a7807b96e2 | ||
|
|
2e9dd5df49 | ||
|
|
1ad16181d0 | ||
|
|
bb53bad1ff | ||
|
|
8a1066cc87 | ||
|
|
23c2dfa78e | ||
|
|
18bf6ac0f6 | ||
|
|
fd6de60fc7 | ||
|
|
4c8ff655d8 | ||
|
|
ac8661c904 | ||
|
|
4f926baf76 | ||
|
|
c5ecf89852 | ||
|
|
a52103620a | ||
|
|
fa5e33e2a5 | ||
|
|
781a94ea91 | ||
|
|
ff1ede9d12 | ||
|
|
90d9a1631a | ||
|
|
c4502bbe16 | ||
|
|
2d4f2671c9 | ||
|
|
e595de2033 | ||
|
|
53875f2791 | ||
|
|
cd7e643228 | ||
|
|
0e1f0d3bfa | ||
|
|
2c76164f6d | ||
|
|
788cd40f2c | ||
|
|
1e12e4e01d | ||
|
|
c4cbfe2a11 | ||
|
|
bbed9e8990 | ||
|
|
354a0b3def | ||
|
|
2faef67b30 | ||
|
|
aee076c854 | ||
|
|
b76f6fa07c | ||
|
|
ec4e9e624c | ||
|
|
a7be2c4602 | ||
|
|
6007d3e99b | ||
|
|
a17b2f20ee | ||
|
|
4375d4caa8 | ||
|
|
c22807bcbb | ||
|
|
baf629a910 | ||
|
|
c4e11d682e | ||
|
|
041ab27b94 | ||
|
|
85aebe8a89 | ||
|
|
3c306a8d7a | ||
|
|
cadaf94cf7 | ||
|
|
15a47a7b4d | ||
|
|
92fca22059 | ||
|
|
0cdc0d2d34 | ||
|
|
1d1dfe103e | ||
|
|
ab39d2cd48 | ||
|
|
32d165d1b3 | ||
|
|
8457f944cd | ||
|
|
c404e92932 | ||
|
|
6af86cae5d | ||
|
|
9421730229 | ||
|
|
b76e31c7e2 | ||
|
|
6056ff2c13 | ||
|
|
8c92a7a75a | ||
|
|
e942f3ef21 | ||
|
|
157da6c0da | ||
|
|
03b909a3f5 | ||
|
|
e08c1491c2 | ||
|
|
95de05ff2d | ||
|
|
e242fa9557 | ||
|
|
b83028fb97 | ||
|
|
7273b5bf42 | ||
|
|
6f6064521b | ||
|
|
48263ec60a | ||
|
|
c6e5b8cbda | ||
|
|
3b8892d2c0 | ||
|
|
54cb757dc5 | ||
|
|
c4de816c7c | ||
|
|
ccb421e110 | ||
|
|
23334e4253 | ||
|
|
826aec2492 | ||
|
|
c397afcf8d | ||
|
|
95c07eab8e | ||
|
|
58e0e5737d | ||
|
|
84bc57e3cf | ||
|
|
67f6169e61 | ||
|
|
6f7c47757b | ||
|
|
0a353e0fa3 | ||
|
|
ddf95da19f | ||
|
|
d5bb16d143 | ||
|
|
d47cfee8bf | ||
|
|
1ee089e7ad | ||
|
|
7255e4b6db | ||
|
|
4e314da86d | ||
|
|
ec06cf10e9 | ||
|
|
ad972185d4 | ||
|
|
bc64b45d5c | ||
|
|
699c2a0e6e | ||
|
|
ebf6e43084 | ||
|
|
6527afb88c | ||
|
|
18e142ed88 | ||
|
|
797d306a4e | ||
|
|
267bf71d47 | ||
|
|
88a89f2178 | ||
|
|
8d2f60eba5 | ||
|
|
b4c9a125e5 | ||
|
|
36c53cfd41 | ||
|
|
d1223e9795 | ||
|
|
cebdbc3aa5 | ||
|
|
be4f235071 | ||
|
|
e8df9e9523 | ||
|
|
9a79308065 | ||
|
|
7a55e76a35 | ||
|
|
c92c55ca52 | ||
|
|
2bb43b57ba | ||
|
|
403a43530f | ||
|
|
51f9024498 | ||
|
|
9f7ad33707 | ||
|
|
fa5241490f | ||
|
|
27fd75aa67 | ||
|
|
c5a583b96f | ||
|
|
f1e301b48e | ||
|
|
61d4c51276 | ||
|
|
cbdbe7bf4b | ||
|
|
baa3a60b05 | ||
|
|
338dafca0c | ||
|
|
b1dddb649b | ||
|
|
3eb284da55 | ||
|
|
5fb969283d | ||
|
|
ae8b5cfe90 | ||
|
|
b6fa7de7e2 | ||
|
|
a4f13ca2de | ||
|
|
5172deb12d | ||
|
|
179f447a70 | ||
|
|
f4a937cc6b | ||
|
|
0b4c6c9af1 | ||
|
|
f17058a344 | ||
|
|
4fcf86a5d6 | ||
|
|
94362d640b | ||
|
|
7192d55732 | ||
|
|
11c9b00510 | ||
|
|
4579d6ca09 | ||
|
|
c3a9937c90 | ||
|
|
8650f82210 | ||
|
|
3ba13d2daa | ||
|
|
dace101c30 | ||
|
|
05e8ec6814 | ||
|
|
646ed6e9b1 | ||
|
|
a501b00ab7 | ||
|
|
6ae683b749 | ||
|
|
235af8c8c6 | ||
|
|
33e4e8e5ce | ||
|
|
20b23a3b0d | ||
|
|
d616c1e386 | ||
|
|
e6f2b23ca9 | ||
|
|
fbfea7ab1f | ||
|
|
bc85fb9597 | ||
|
|
a34f06669d | ||
|
|
6508c2b62c | ||
|
|
fb53b9e8ea | ||
|
|
75abab2832 | ||
|
|
13d9fa87d8 | ||
|
|
c0d40b2594 | ||
|
|
327cfe7d21 | ||
|
|
b4e83fd3f0 | ||
|
|
c902c1eace | ||
|
|
39a787c3c9 | ||
|
|
cdc20ce500 | ||
|
|
7dcd7badb3 | ||
|
|
1a0a73ac7c | ||
|
|
eb1fb776a5 | ||
|
|
82349d380b | ||
|
|
3c5deca55e | ||
|
|
e1b9e0165a | ||
|
|
2e701d0d23 | ||
|
|
259e3d716b | ||
|
|
43398449c7 | ||
|
|
e7f00fe088 | ||
|
|
1a9364acae | ||
|
|
19234998f1 | ||
|
|
31037775d1 | ||
|
|
e0b1bb0d06 | ||
|
|
1f810aa36f | ||
|
|
e7a8cec0b4 | ||
|
|
ae07ac2598 | ||
|
|
d75098fdd9 | ||
|
|
ac87cc13d7 | ||
|
|
34bffe0148 | ||
|
|
15f3cf07a8 | ||
|
|
c0f0d45138 | ||
|
|
2eddaccb86 | ||
|
|
87b2f4f93a | ||
|
|
88970314ad | ||
|
|
0de4e2afcc | ||
|
|
2671ac18c8 | ||
|
|
68a992c79e | ||
|
|
9bf233ec79 | ||
|
|
b41dcded98 | ||
|
|
b6239ed608 | ||
|
|
9dfe613921 | ||
|
|
865119e2ae | ||
|
|
c616f9c92a | ||
|
|
2737a85a61 | ||
|
|
c88979a6dd | ||
|
|
a6f7c9b3ef | ||
|
|
2aac68b412 | ||
|
|
e1b07868fd | ||
|
|
aaec886b81 | ||
|
|
a7a4b55011 | ||
|
|
bcb7e03aac | ||
|
|
c49a559e34 | ||
|
|
af98eb5e01 | ||
|
|
e47c3efc58 | ||
|
|
058c3e18d2 | ||
|
|
38ae19cbec | ||
|
|
2eeffb374f | ||
|
|
e478fa43c8 | ||
|
|
1093f92d67 | ||
|
|
6148dc6a89 | ||
|
|
a20ade9016 | ||
|
|
fbbb7bd3a7 | ||
|
|
0c44160023 | ||
|
|
13427cdab7 | ||
|
|
d0ffc0308c | ||
|
|
fe62e03367 | ||
|
|
5deb41c651 | ||
|
|
6b454c299d | ||
|
|
dddf4c8e38 | ||
|
|
0912de74e9 | ||
|
|
3752e5b9e0 | ||
|
|
ae62d5771f | ||
|
|
434749a250 | ||
|
|
cb9644d694 | ||
|
|
8f7e53a37c | ||
|
|
b158e70495 | ||
|
|
ebed4f972a | ||
|
|
f8b15d9591 | ||
|
|
ab1e86d4ca | ||
|
|
9da577aa60 | ||
|
|
d7f690bd9a | ||
|
|
0f52bbd170 | ||
|
|
61c62032f2 | ||
|
|
eb6be6b589 | ||
|
|
25f262c6e4 | ||
|
|
f1cca0b305 | ||
|
|
6c9a497725 | ||
|
|
d87b6aa505 | ||
|
|
269dd7e0a7 | ||
|
|
f9c55ee567 | ||
|
|
8192f7a0bb | ||
|
|
5b5afe7aa8 | ||
|
|
c0dfabf0f8 | ||
|
|
2491fba863 | ||
|
|
f3d16556ff | ||
|
|
474cc1eb3e | ||
|
|
a66af36224 | ||
|
|
4471746842 | ||
|
|
9944514fab | ||
|
|
4dc60784c1 | ||
|
|
2c75c051a7 | ||
|
|
5a5cfa4ca0 | ||
|
|
f96b3251cd |
No files matched your search
@@ -0,0 +1,52 @@
|
||||
# Java Maven CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-java/ for more details
|
||||
#
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/openjdk:11.0
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
# - image: circleci/postgres:9.4
|
||||
|
||||
working_directory: ~/repo
|
||||
|
||||
environment:
|
||||
# Customize the JVM maximum heap limit
|
||||
MAVEN_OPTS: -Xmx3200m
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "pom.xml" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run: mvn dependency:go-offline
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
|
||||
# run tests!
|
||||
- run: mvn integration-test
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- gh-pages
|
||||
- gh-pages-generated
|
||||
@@ -0,0 +1 @@
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
@@ -0,0 +1,9 @@
|
||||
/nbproject/
|
||||
/target/
|
||||
/Playlists/
|
||||
/test/
|
||||
*.json
|
||||
*.txt
|
||||
nb*.xml
|
||||
dockerfile
|
||||
.*!.mvn/
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
@@ -1,201 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017 John Grosh
|
||||
|
||||
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.
|
||||
@@ -1,5 +1,103 @@
|
||||
# Spectra-Music
|
||||
A Discord music bot with a clean interface, and that is easy to set up and run yourself
|
||||
<img align="right" src="https://i.imgur.com/zrE80HY.png" height="200" width="200">
|
||||
|
||||
## Note:
|
||||
This project is currently in early stages of development. If you are having trouble setting it up, or feel that it isn't complete, that's because it is not fully supported yet.
|
||||
# JMusicBot (TrueOG Network fork)
|
||||
|
||||
A self-hosted Discord music bot, forked by the [TrueOG Network](https://true-og.net) from [chrisb09/MusicBot](https://github.com/chrisb09/MusicBot), which is in turn a modernised fork of [jagrosh/MusicBot](https://github.com/jagrosh/MusicBot).
|
||||
|
||||
The upstream fork keeps the bot working on current Discord, JDA, and YouTube; this fork keeps it running on TrueOG's infrastructure.
|
||||
|
||||
## TrueOG Network Fork Changes
|
||||
|
||||
- Maven Wrapper (`./mvnw`) so builds use a pinned Maven version instead of whatever is installed
|
||||
- Selenium bumped to 4.49.0 with the CDP v153 devtools module, so YouTube PO-token extraction works with current Chromium
|
||||
- `maven-shade-plugin` now merges `META-INF/services`, so bundled `CdpInfo` (and other SPI) registrations are no longer silently dropped from the fat jar
|
||||
- YouTube PO-token capture handles the gzip-compressed `/youtubei/v1/player` request body that YouTube web client 2.20260918+ sends, and logs a warning instead of silently timing out when the body cannot be parsed
|
||||
|
||||
## Inherited From Upstream (chrisb09)
|
||||
|
||||
- JDA 6 support and current dependency updates
|
||||
- DAVE voice encryption support for current Discord voice requirements
|
||||
- Optional YouTube account login, PO token, and visitor-data support
|
||||
- Optional per-channel playback status message
|
||||
- Optional JDBC/SQLite playback analytics logging
|
||||
- Regular [lavaplayer](https://mvnrepository.com/artifact/dev.arbjerg/lavaplayer)/[lavalink](https://maven.lavalink.dev/#/releases/dev/lavalink/youtube/common) bumps
|
||||
|
||||
## Quick Links
|
||||
|
||||
- Setup guide: [docs/setup.md](/docs/setup.md)
|
||||
- Upstream fork changelog: [docs/fork-changelog.md](/docs/fork-changelog.md)
|
||||
|
||||
## Features
|
||||
|
||||
- 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
|
||||
|
||||
## 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
|
||||
- MP4/M4A
|
||||
- OGG streams
|
||||
- AAC streams
|
||||
- Stream playlists such as M3U and PLS
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
The runnable jar is written to `target/JMusicBot-<version>-All.jar`.
|
||||
|
||||
## Setup Notes
|
||||
|
||||
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: `java -Dnogui=true -jar target/JMusicBot-<version>-All.jar`
|
||||
|
||||
For the detailed version, see [docs/setup.md](/docs/setup.md).
|
||||
|
||||
## YouTube Playback
|
||||
|
||||
YouTube requires a PO token, which the bot obtains at startup by driving a headless Chromium through Selenium. Chromium must be installed, and the bundled Selenium CDP module must match the installed Chromium major version (currently v153 — see `pom.xml`). If Chromium is upgraded past that, bump `selenium-java` and the `selenium-devtools-vNNN` exclusions in `pom.xml` and rebuild.
|
||||
|
||||
Google account login is also supported as a fallback:
|
||||
|
||||
```conf
|
||||
youtubeoauth2=true
|
||||
```
|
||||
|
||||
Do not use your main Google account. Use a burner or otherwise disposable account instead. Details are in [docs/setup.md](/docs/setup.md).
|
||||
|
||||
## DAVE / Voice Encryption
|
||||
|
||||
Discord requires DAVE for voice on modern API versions. The bot 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](/pom.xml).
|
||||
|
||||
## 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)
|
||||
@@ -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
@@ -0,0 +1,178 @@
|
||||
# Setup Guide
|
||||
|
||||
This guide covers the normal 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
|
||||
./mvnw 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 on Windows, 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).
|
||||
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -0,0 +1,302 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.jagrosh</groupId>
|
||||
<artifactId>JMusicBot</artifactId>
|
||||
<version>0.6.7</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>
|
||||
<url>https://m2.dv8tion.net/releases</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<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>6.3.1</version>
|
||||
</dependency>
|
||||
<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>2.2.7</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>f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jagrosh</groupId>
|
||||
<artifactId>JLyrics</artifactId>
|
||||
<version>master</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.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>
|
||||
<artifactId>config</artifactId>
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.14.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>4.49.0</version>
|
||||
<!-- https://github.com/SeleniumHQ/selenium/issues/13791 -->
|
||||
<!-- selenium-java 4.49.0 ships CDP v151/v152/v153; keep only the one matching the installed Chromium -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-devtools-v151</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-devtools-v152</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>
|
||||
<version>4.13.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
<version>1.3</version>
|
||||
<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>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>1.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>All</shadedClassifierName>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>*:*</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>reference.conf</resource>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>com.jagrosh.jmusicbot.JMusicBot</Main-Class>
|
||||
<Specification-Title>${project.artifactId}</Specification-Title>
|
||||
<Specification-Version>${project.version}</Specification-Version>
|
||||
<Implementation-Title>${project.artifactId}</Implementation-Title>
|
||||
<Implementation-Version>${project.version}</Implementation-Version>
|
||||
<Implementation-Vendor-Id>${project.groupId}</Implementation-Vendor-Id>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jda.chewtils.version>2.2.1</jda.chewtils.version>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This will have this script check for a new version of JMusicBot every
|
||||
# startup (and download it if the latest version isn't currently downloaded)
|
||||
DOWNLOAD=true
|
||||
|
||||
# This will cause the script to run in a loop so that the bot auto-restarts
|
||||
# when you use the shutdown command
|
||||
LOOP=true
|
||||
|
||||
download() {
|
||||
if [ $DOWNLOAD = true ]; then
|
||||
URL=$(curl -s https://api.github.com/repos/jagrosh/MusicBot/releases/latest \
|
||||
| grep -i "browser_download_url.*\.jar" \
|
||||
| sed 's/.*\(http.*\)"/\1/')
|
||||
FILENAME=$(echo $URL | sed 's/.*\/\([^\/]*\)/\1/')
|
||||
if [ -f $FILENAME ]; then
|
||||
echo "Latest version already downloaded (${FILENAME})"
|
||||
else
|
||||
curl -L $URL -o $FILENAME
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run() {
|
||||
java -Dnogui=true -jar $(ls -t JMusicBot* | head -1)
|
||||
}
|
||||
|
||||
while
|
||||
download
|
||||
run
|
||||
$LOOP
|
||||
do
|
||||
continue
|
||||
done
|
||||
@@ -0,0 +1 @@
|
||||
jda-chewtils.version=${jda.chewtils.version}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2018 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 java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class Bot
|
||||
{
|
||||
private final EventWaiter waiter;
|
||||
private final ScheduledExecutorService threadpool;
|
||||
private final BotConfig config;
|
||||
private final SettingsManager settings;
|
||||
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;
|
||||
private GUI gui;
|
||||
|
||||
public Bot(EventWaiter waiter, BotConfig config, SettingsManager settings)
|
||||
{
|
||||
this.waiter = waiter;
|
||||
this.config = config;
|
||||
this.settings = settings;
|
||||
this.playlists = new PlaylistLoader(config);
|
||||
this.threadpool = Executors.newSingleThreadScheduledExecutor();
|
||||
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()
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
public SettingsManager getSettingsManager()
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
public EventWaiter getWaiter()
|
||||
{
|
||||
return waiter;
|
||||
}
|
||||
|
||||
public ScheduledExecutorService getThreadpool()
|
||||
{
|
||||
return threadpool;
|
||||
}
|
||||
|
||||
public PlayerManager getPlayerManager()
|
||||
{
|
||||
return players;
|
||||
}
|
||||
|
||||
public PlaylistLoader getPlaylistLoader()
|
||||
{
|
||||
return playlists;
|
||||
}
|
||||
|
||||
public NowplayingHandler getNowplayingHandler()
|
||||
{
|
||||
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()
|
||||
{
|
||||
return jda;
|
||||
}
|
||||
|
||||
public void closeAudioConnection(long guildId)
|
||||
{
|
||||
Guild guild = jda.getGuildById(guildId);
|
||||
if(guild!=null)
|
||||
threadpool.submit(() -> guild.getAudioManager().closeAudioConnection());
|
||||
}
|
||||
|
||||
public void resetGame()
|
||||
{
|
||||
Activity game = config.getGame()==null || config.getGame().getName().equalsIgnoreCase("none") ? null : config.getGame();
|
||||
if(!Objects.equals(jda.getPresence().getActivity(), game))
|
||||
jda.getPresence().setActivity(game);
|
||||
}
|
||||
|
||||
public void shutdown()
|
||||
{
|
||||
if(shuttingDown)
|
||||
return;
|
||||
shuttingDown = true;
|
||||
threadpool.shutdownNow();
|
||||
if(jda != null && jda.getStatus()!=JDA.Status.SHUTTING_DOWN)
|
||||
{
|
||||
jda.getGuilds().stream().forEach(g ->
|
||||
{
|
||||
g.getAudioManager().closeAudioConnection();
|
||||
AudioHandler ah = (AudioHandler)g.getAudioManager().getSendingHandler();
|
||||
if(ah!=null)
|
||||
{
|
||||
ah.stopAndClear();
|
||||
ah.getPlayer().destroy();
|
||||
}
|
||||
});
|
||||
jda.shutdown();
|
||||
}
|
||||
if(gui!=null)
|
||||
gui.dispose();
|
||||
if(dataLogService != null)
|
||||
dataLogService.shutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public void setJDA(JDA jda)
|
||||
{
|
||||
this.jda = jda;
|
||||
}
|
||||
|
||||
public void setGUI(GUI gui)
|
||||
{
|
||||
this.gui = gui;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright 2018 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;
|
||||
|
||||
import com.jagrosh.jmusicbot.entities.Prompt;
|
||||
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.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Activity;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author John Grosh (jagrosh)
|
||||
*/
|
||||
public class BotConfig
|
||||
{
|
||||
private final Prompt prompt;
|
||||
private final static String CONTEXT = "Config";
|
||||
private final static String START_TOKEN = "/// START OF JMUSICBOT CONFIG ///";
|
||||
private final static String END_TOKEN = "/// END OF JMUSICBOT CONFIG ///";
|
||||
|
||||
private Path path = null;
|
||||
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;
|
||||
|
||||
private boolean valid = false;
|
||||
|
||||
public BotConfig(Prompt prompt)
|
||||
{
|
||||
this.prompt = prompt;
|
||||
}
|
||||
|
||||
public void load()
|
||||
{
|
||||
valid = false;
|
||||
|
||||
// read config from file
|
||||
try
|
||||
{
|
||||
// get the path to the config, default config.txt
|
||||
path = getConfigPath();
|
||||
|
||||
// load in the config file, plus the default values
|
||||
//Config config = ConfigFactory.parseFile(path.toFile()).withFallback(ConfigFactory.load());
|
||||
Config config = ConfigFactory.load();
|
||||
|
||||
// set values
|
||||
token = config.getString("token");
|
||||
prefix = config.getString("prefix");
|
||||
altprefix = config.getString("altprefix");
|
||||
helpWord = config.getString("help");
|
||||
owner = config.getLong("owner");
|
||||
successEmoji = config.getString("success");
|
||||
warningEmoji = config.getString("warning");
|
||||
errorEmoji = config.getString("error");
|
||||
loadingEmoji = config.getString("loading");
|
||||
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;
|
||||
|
||||
// validate bot token
|
||||
if(token==null || token.isEmpty() || token.equalsIgnoreCase("BOT_TOKEN_HERE"))
|
||||
{
|
||||
token = prompt.prompt("Please provide a bot token."
|
||||
+ "\nInstructions for obtaining a token can be found here:"
|
||||
+ "\nhttps://github.com/jagrosh/MusicBot/wiki/Getting-a-Bot-Token."
|
||||
+ "\nBot Token: ");
|
||||
if(token==null)
|
||||
{
|
||||
prompt.alert(Prompt.Level.WARNING, CONTEXT, "No token provided! Exiting.\n\nConfig Location: " + path.toAbsolutePath().toString());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
write = true;
|
||||
}
|
||||
}
|
||||
|
||||
// validate bot owner
|
||||
if(owner<=0)
|
||||
{
|
||||
try
|
||||
{
|
||||
owner = Long.parseLong(prompt.prompt("Owner ID was missing, or the provided owner ID is not valid."
|
||||
+ "\nPlease provide the User ID of the bot's owner."
|
||||
+ "\nInstructions for obtaining your User ID can be found here:"
|
||||
+ "\nhttps://github.com/jagrosh/MusicBot/wiki/Finding-Your-User-ID"
|
||||
+ "\nOwner User ID: "));
|
||||
}
|
||||
catch(NumberFormatException | NullPointerException ex)
|
||||
{
|
||||
owner = 0;
|
||||
}
|
||||
if(owner<=0)
|
||||
{
|
||||
prompt.alert(Prompt.Level.ERROR, CONTEXT, "Invalid User ID! Exiting.\n\nConfig Location: " + path.toAbsolutePath().toString());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
write = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(write)
|
||||
writeToFile();
|
||||
|
||||
// if we get through the whole config, it's good to go
|
||||
valid = true;
|
||||
}
|
||||
catch (ConfigException ex)
|
||||
{
|
||||
prompt.alert(Prompt.Level.ERROR, CONTEXT, ex + ": " + ex.getMessage() + "\n\nConfig Location: " + path.toAbsolutePath().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeToFile()
|
||||
{
|
||||
byte[] bytes = loadDefaultConfig().replace("BOT_TOKEN_HERE", token)
|
||||
.replace("0 // OWNER ID", Long.toString(owner))
|
||||
.trim().getBytes();
|
||||
try
|
||||
{
|
||||
Files.write(path, bytes);
|
||||
}
|
||||
catch(IOException ex)
|
||||
{
|
||||
prompt.alert(Prompt.Level.WARNING, CONTEXT, "Failed to write new config options to config.txt: "+ex
|
||||
+ "\nPlease make sure that the files are not on your desktop or some other restricted area.\n\nConfig Location: "
|
||||
+ path.toAbsolutePath().toString());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public String getConfigLocation()
|
||||
{
|
||||
return path.toFile().getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getPrefix()
|
||||
{
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public String getAltPrefix()
|
||||
{
|
||||
return "NONE".equalsIgnoreCase(altprefix) ? null : altprefix;
|
||||
}
|
||||
|
||||
public String getToken()
|
||||
{
|
||||
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()
|
||||
{
|
||||
return successEmoji;
|
||||
}
|
||||
|
||||
public String getWarning()
|
||||
{
|
||||
return warningEmoji;
|
||||
}
|
||||
|
||||
public String getError()
|
||||
{
|
||||
return errorEmoji;
|
||||
}
|
||||
|
||||
public String getLoading()
|
||||
{
|
||||
return loadingEmoji;
|
||||
}
|
||||
|
||||
public String getSearching()
|
||||
{
|
||||
return searchingEmoji;
|
||||
}
|
||||
|
||||
public Activity getGame()
|
||||
{
|
||||
return game;
|
||||
}
|
||||
|
||||
public boolean isGameNone()
|
||||
{
|
||||
return game != null && game.getName().equalsIgnoreCase("none");
|
||||
}
|
||||
|
||||
public OnlineStatus getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getHelp()
|
||||
{
|
||||
return helpWord;
|
||||
}
|
||||
|
||||
public boolean useYoutubeOauth2()
|
||||
{
|
||||
return youtubeOauth2;
|
||||
}
|
||||
|
||||
public boolean getStay()
|
||||
{
|
||||
return stayInChannel;
|
||||
}
|
||||
|
||||
public boolean getSongInStatus()
|
||||
{
|
||||
return songInGame;
|
||||
}
|
||||
|
||||
public boolean useStatusMessages()
|
||||
{
|
||||
return statusMessages;
|
||||
}
|
||||
|
||||
public String getPlaylistsFolder()
|
||||
{
|
||||
return playlistsFolder;
|
||||
}
|
||||
|
||||
public boolean getDBots()
|
||||
{
|
||||
return dbots;
|
||||
}
|
||||
|
||||
public boolean useUpdateAlerts()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public long getMaxSeconds()
|
||||
{
|
||||
return maxSeconds;
|
||||
}
|
||||
|
||||
public int getMaxYTPlaylistPages()
|
||||
{
|
||||
return maxYTPlaylistPages;
|
||||
}
|
||||
|
||||
public String getMaxTime()
|
||||
{
|
||||
return TimeUtil.formatTime(maxSeconds * 1000);
|
||||
}
|
||||
|
||||
public long getAloneTimeUntilStop()
|
||||
{
|
||||
return aloneTimeUntilStop;
|
||||
}
|
||||
|
||||
public boolean isTooLong(AudioTrack track)
|
||||
{
|
||||
if(maxSeconds<=0)
|
||||
return false;
|
||||
return Math.round(track.getDuration()/1000.0) > maxSeconds;
|
||||
}
|
||||
|
||||
public String[] getAliases(String command)
|
||||
{
|
||||
try
|
||||
{
|
||||
return aliases.getStringList(command).toArray(new String[0]);
|
||||
}
|
||||
catch(NullPointerException | ConfigException.Missing e)
|
||||
{
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
||||
public Config getTransforms()
|
||||
{
|
||||
return transforms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright 2016 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;
|
||||
|
||||
import com.jagrosh.jdautilities.command.CommandClient;
|
||||
import com.jagrosh.jdautilities.command.CommandClientBuilder;
|
||||
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
|
||||
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.*;
|
||||
import com.jagrosh.jmusicbot.commands.music.*;
|
||||
import com.jagrosh.jmusicbot.commands.owner.*;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (jagrosh)
|
||||
*/
|
||||
public class JMusicBot
|
||||
{
|
||||
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.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)
|
||||
{
|
||||
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");
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
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);
|
||||
aboutCommand.setReplacementCharacter("\uD83C\uDFB6"); // 🎶
|
||||
|
||||
// set up the command client
|
||||
CommandClientBuilder cb = new CommandClientBuilder()
|
||||
.setPrefix(config.getPrefix())
|
||||
.setAlternativePrefix(config.getAltPrefix())
|
||||
.setOwnerId(Long.toString(config.getOwnerId()))
|
||||
.setEmojis(config.getSuccess(), config.getWarning(), config.getError())
|
||||
.setHelpWord(config.getHelp())
|
||||
.setLinkedCacheSize(200)
|
||||
.setGuildSettingsManager(settings)
|
||||
.addCommands(aboutCommand,
|
||||
new LoggedPingCmd(bot),
|
||||
new SettingsCmd(bot),
|
||||
new StatsCmd(bot),
|
||||
|
||||
new LyricsCmd(bot),
|
||||
new NowplayingCmd(bot),
|
||||
new PlayCmd(bot),
|
||||
new PlaylistsCmd(bot),
|
||||
new QueueCmd(bot),
|
||||
new RemoveCmd(bot),
|
||||
new SearchCmd(bot),
|
||||
new SCSearchCmd(bot),
|
||||
new SeekCmd(bot),
|
||||
new ShuffleCmd(bot),
|
||||
new SkipCmd(bot),
|
||||
|
||||
new ForceRemoveCmd(bot),
|
||||
new ForceskipCmd(bot),
|
||||
new MoveTrackCmd(bot),
|
||||
new PauseCmd(bot),
|
||||
new PlaynextCmd(bot),
|
||||
new RepeatCmd(bot),
|
||||
new SkiptoCmd(bot),
|
||||
new StopCmd(bot),
|
||||
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),
|
||||
new SetavatarCmd(bot),
|
||||
new SetgameCmd(bot),
|
||||
new SetnameCmd(bot),
|
||||
new SetstatusCmd(bot),
|
||||
new ShutdownCmd(bot)
|
||||
);
|
||||
|
||||
// enable eval if applicable
|
||||
if(config.useEval())
|
||||
cb.addCommand(new EvalCmd(bot));
|
||||
|
||||
// set status if set in config
|
||||
if(config.getStatus() != OnlineStatus.UNKNOWN)
|
||||
cb.setStatus(config.getStatus());
|
||||
|
||||
// set game
|
||||
if(config.getGame() == null)
|
||||
cb.useDefaultGame();
|
||||
else if(config.isGameNone())
|
||||
cb.setActivity(null);
|
||||
else
|
||||
cb.setActivity(config.getGame());
|
||||
|
||||
return cb.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2016 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.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.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.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;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class Listener extends ListenerAdapter
|
||||
{
|
||||
private final Bot bot;
|
||||
|
||||
public Listener(Bot bot)
|
||||
{
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady(ReadyEvent event)
|
||||
{
|
||||
if(event.getJDA().getGuildCache().isEmpty())
|
||||
{
|
||||
Logger log = LoggerFactory.getLogger("MusicBot");
|
||||
log.warn("This bot is not on any guilds! Use the following link to add the bot to your guilds!");
|
||||
log.warn(event.getJDA().getInviteUrl(JMusicBot.RECOMMENDED_PERMS));
|
||||
}
|
||||
credit(event.getJDA());
|
||||
event.getJDA().getGuilds().forEach((guild) ->
|
||||
{
|
||||
try
|
||||
{
|
||||
String defpl = bot.getSettingsManager().getSettings(guild).getDefaultPlaylist();
|
||||
VoiceChannel vc = bot.getSettingsManager().getSettings(guild).getVoiceChannel(guild);
|
||||
if(defpl!=null && vc!=null && bot.getPlayerManager().setUpHandler(guild).playFromDefault())
|
||||
{
|
||||
guild.getAudioManager().openAudioConnection(vc);
|
||||
}
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
});
|
||||
if(bot.getConfig().useUpdateAlerts())
|
||||
{
|
||||
bot.getThreadpool().scheduleWithFixedDelay(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
User owner = bot.getJDA().retrieveUserById(bot.getConfig().getOwnerId()).complete();
|
||||
String currentVersion = OtherUtil.getCurrentVersion();
|
||||
String latestVersion = OtherUtil.getLatestVersion();
|
||||
if(latestVersion!=null && !currentVersion.equalsIgnoreCase(latestVersion))
|
||||
{
|
||||
String msg = String.format(OtherUtil.NEW_VERSION_AVAILABLE, currentVersion, latestVersion);
|
||||
owner.openPrivateChannel().queue(pc -> pc.sendMessage(msg).queue());
|
||||
}
|
||||
}
|
||||
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 onMessageDelete(MessageDeleteEvent event)
|
||||
{
|
||||
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
|
||||
public void onShutdown(ShutdownEvent event)
|
||||
{
|
||||
bot.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildJoin(GuildJoinEvent event)
|
||||
{
|
||||
credit(event.getJDA());
|
||||
}
|
||||
|
||||
// make sure people aren't adding clones to dbots
|
||||
private void credit(JDA jda)
|
||||
{
|
||||
Guild dbots = jda.getGuildById(110373943822540800L);
|
||||
if(dbots==null)
|
||||
return;
|
||||
if(bot.getConfig().getDBots())
|
||||
return;
|
||||
jda.getTextChannelById(119222314964353025L)
|
||||
.sendMessage("This account is running JMusicBot. Please do not list bot clones on this server, <@"+bot.getConfig().getOwnerId()+">.").complete();
|
||||
dbots.leave().queue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2021 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.audio;
|
||||
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Michaili K (mysteriouscursor+git@protonmail.com)
|
||||
*/
|
||||
public class AloneInVoiceHandler
|
||||
{
|
||||
private final Bot bot;
|
||||
private final HashMap<Long, Instant> aloneSince = new HashMap<>();
|
||||
private long aloneTimeUntilStop = 0;
|
||||
|
||||
public AloneInVoiceHandler(Bot bot)
|
||||
{
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
public void init()
|
||||
{
|
||||
aloneTimeUntilStop = bot.getConfig().getAloneTimeUntilStop();
|
||||
if(aloneTimeUntilStop > 0)
|
||||
bot.getThreadpool().scheduleWithFixedDelay(() -> check(), 0, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void check()
|
||||
{
|
||||
Set<Long> toRemove = new HashSet<>();
|
||||
for(Map.Entry<Long, Instant> entrySet: aloneSince.entrySet())
|
||||
{
|
||||
if(entrySet.getValue().getEpochSecond() > Instant.now().getEpochSecond() - aloneTimeUntilStop) continue;
|
||||
|
||||
Guild guild = bot.getJDA().getGuildById(entrySet.getKey());
|
||||
|
||||
if(guild == null)
|
||||
{
|
||||
toRemove.add(entrySet.getKey());
|
||||
continue;
|
||||
}
|
||||
|
||||
((AudioHandler) guild.getAudioManager().getSendingHandler()).stopAndClear();
|
||||
guild.getAudioManager().closeAudioConnection();
|
||||
|
||||
toRemove.add(entrySet.getKey());
|
||||
}
|
||||
toRemove.forEach(id -> aloneSince.remove(id));
|
||||
}
|
||||
|
||||
public void onVoiceUpdate(GuildVoiceUpdateEvent event)
|
||||
{
|
||||
if(aloneTimeUntilStop <= 0) return;
|
||||
|
||||
Guild guild = event.getEntity().getGuild();
|
||||
if(!bot.getPlayerManager().hasHandler(guild)) return;
|
||||
|
||||
boolean alone = isAlone(guild);
|
||||
boolean inList = aloneSince.containsKey(guild.getIdLong());
|
||||
|
||||
if(!alone && inList)
|
||||
aloneSince.remove(guild.getIdLong());
|
||||
else if(alone && !inList)
|
||||
aloneSince.put(guild.getIdLong(), Instant.now());
|
||||
}
|
||||
|
||||
private boolean isAlone(Guild guild)
|
||||
{
|
||||
if(guild.getAudioManager().getConnectedChannel() == null) return false;
|
||||
return guild.getAudioManager().getConnectedChannel().getMembers().stream()
|
||||
.noneMatch(x ->
|
||||
!x.getVoiceState().isDeafened()
|
||||
&& !x.getUser().isBot());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* Copyright 2016 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.audio;
|
||||
|
||||
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;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
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.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.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
|
||||
{
|
||||
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<>();
|
||||
|
||||
private final PlayerManager manager;
|
||||
private final AudioPlayer audioPlayer;
|
||||
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)
|
||||
{
|
||||
if(audioPlayer.getPlayingTrack()==null)
|
||||
{
|
||||
audioPlayer.playTrack(qtrack.getTrack());
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
queue.addAt(0, qtrack);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int addTrack(QueuedTrack qtrack)
|
||||
{
|
||||
if(audioPlayer.getPlayingTrack()==null)
|
||||
{
|
||||
audioPlayer.playTrack(qtrack.getTrack());
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
return queue.add(qtrack);
|
||||
}
|
||||
|
||||
public AbstractQueue<QueuedTrack> getQueue()
|
||||
{
|
||||
return queue;
|
||||
}
|
||||
|
||||
public void stopAndClear()
|
||||
{
|
||||
queue.clear();
|
||||
defaultQueue.clear();
|
||||
audioPlayer.stopTrack();
|
||||
//current = null;
|
||||
}
|
||||
|
||||
public boolean isMusicPlaying(JDA jda)
|
||||
{
|
||||
return guild(jda).getSelfMember().getVoiceState().inAudioChannel() && audioPlayer.getPlayingTrack()!=null;
|
||||
}
|
||||
|
||||
public Set<String> getVotes()
|
||||
{
|
||||
return votes;
|
||||
}
|
||||
|
||||
public AudioPlayer getPlayer()
|
||||
{
|
||||
return audioPlayer;
|
||||
}
|
||||
|
||||
public RequestMetadata getRequestMetadata()
|
||||
{
|
||||
if(audioPlayer.getPlayingTrack() == null)
|
||||
return RequestMetadata.EMPTY;
|
||||
RequestMetadata rm = audioPlayer.getPlayingTrack().getUserData(RequestMetadata.class);
|
||||
return rm == null ? RequestMetadata.EMPTY : rm;
|
||||
}
|
||||
|
||||
public boolean playFromDefault()
|
||||
{
|
||||
if(!defaultQueue.isEmpty())
|
||||
{
|
||||
audioPlayer.playTrack(defaultQueue.remove(0));
|
||||
return true;
|
||||
}
|
||||
Settings settings = manager.getBot().getSettingsManager().getSettings(guildId);
|
||||
if(settings==null || settings.getDefaultPlaylist()==null)
|
||||
return false;
|
||||
|
||||
Playlist pl = manager.getBot().getPlaylistLoader().getPlaylist(settings.getDefaultPlaylist());
|
||||
if(pl==null || pl.getItems().isEmpty())
|
||||
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())
|
||||
manager.getBot().closeAudioConnection(guildId);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Audio Events
|
||||
@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)
|
||||
{
|
||||
QueuedTrack clone = new QueuedTrack(track.makeClone(), track.getUserData(RequestMetadata.class));
|
||||
if(repeatMode == RepeatMode.ALL)
|
||||
queue.add(clone);
|
||||
else
|
||||
queue.addAt(0, clone);
|
||||
}
|
||||
|
||||
if(queue.isEmpty())
|
||||
{
|
||||
if(!playFromDefault())
|
||||
{
|
||||
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.
|
||||
// this is to prevent the player being paused next time it's being used.
|
||||
player.setPaused(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QueuedTrack qt = queue.pull();
|
||||
player.playTrack(qt.getTrack());
|
||||
}
|
||||
}
|
||||
|
||||
@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(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 MessageCreateData getNowPlaying(JDA jda)
|
||||
{
|
||||
if(isMusicPlaying(jda))
|
||||
{
|
||||
Guild guild = guild(jda);
|
||||
AudioTrack track = audioPlayer.getPlayingTrack();
|
||||
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();
|
||||
if(rm.getOwner() != 0L)
|
||||
{
|
||||
User u = guild.getJDA().getUserById(rm.user.id);
|
||||
if(u==null)
|
||||
eb.setAuthor(FormatUtil.formatUsername(rm.user), null, rm.user.avatar);
|
||||
else
|
||||
eb.setAuthor(FormatUtil.formatUsername(u), null, u.getEffectiveAvatarUrl());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
eb.setTitle(track.getInfo().title, track.getInfo().uri);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
eb.setTitle(track.getInfo().title);
|
||||
}
|
||||
|
||||
if(track instanceof YoutubeAudioTrack && manager.getBot().getConfig().useNPImages())
|
||||
{
|
||||
eb.setThumbnail("https://img.youtube.com/vi/"+track.getIdentifier()+"/mqdefault.jpg");
|
||||
}
|
||||
|
||||
if(track.getInfo().author != null && !track.getInfo().author.isEmpty())
|
||||
eb.setFooter("Source: " + track.getInfo().author, null);
|
||||
|
||||
double progress = (double)audioPlayer.getPlayingTrack().getPosition()/track.getDuration();
|
||||
eb.setDescription(getStatusEmoji()
|
||||
+ " "+FormatUtil.progressBar(progress)
|
||||
+ " `[" + TimeUtil.formatTime(track.getPosition()) + "/" + TimeUtil.formatTime(track.getDuration()) + "]` "
|
||||
+ FormatUtil.volumeIcon(audioPlayer.getVolume()));
|
||||
if(currentTrackServerPlayCount > 0L && track.getIdentifier().equals(playCountTrackIdentifier))
|
||||
eb.addField("Server plays", "Played " + currentTrackServerPlayCount + " time" + (currentTrackServerPlayCount == 1L ? "" : "s") + " on this server", false);
|
||||
|
||||
return mb.setEmbeds(eb.build()).build();
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
|
||||
public MessageCreateData getNoMusicPlaying(JDA jda)
|
||||
{
|
||||
Guild guild = guild(jda);
|
||||
return new MessageCreateBuilder()
|
||||
.setContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing...**"))
|
||||
.setEmbeds(new EmbedBuilder()
|
||||
.setTitle("No music playing")
|
||||
.setDescription(STOP_EMOJI+" "+FormatUtil.progressBar(-1)+" "+FormatUtil.volumeIcon(audioPlayer.getVolume()))
|
||||
.setColor(guild.getSelfMember().getColor())
|
||||
.build()).build();
|
||||
}
|
||||
|
||||
public MessageCreateData getStatusMessage(JDA 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
eb.setTitle(track.getInfo().title, track.getInfo().uri);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
eb.setTitle(track.getInfo().title);
|
||||
}
|
||||
}
|
||||
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
|
||||
/*@Override
|
||||
public boolean canProvide()
|
||||
{
|
||||
if (lastFrame == null)
|
||||
lastFrame = audioPlayer.provide();
|
||||
|
||||
return lastFrame != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] provide20MsAudio()
|
||||
{
|
||||
if (lastFrame == null)
|
||||
lastFrame = audioPlayer.provide();
|
||||
|
||||
byte[] data = lastFrame != null ? lastFrame.getData() : null;
|
||||
lastFrame = null;
|
||||
|
||||
return data;
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public boolean canProvide()
|
||||
{
|
||||
lastFrame = audioPlayer.provide();
|
||||
return lastFrame != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer provide20MsAudio()
|
||||
{
|
||||
return ByteBuffer.wrap(lastFrame.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpus()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Private methods
|
||||
private Guild guild(JDA jda)
|
||||
{
|
||||
return jda.getGuildById(guildId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2018 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.audio;
|
||||
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import com.jagrosh.jmusicbot.entities.Pair;
|
||||
import com.jagrosh.jmusicbot.settings.Settings;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class NowplayingHandler
|
||||
{
|
||||
private final Bot bot;
|
||||
private final HashMap<Long,Pair<Long,Long>> lastNP; // guild -> channel,message
|
||||
|
||||
public NowplayingHandler(Bot bot)
|
||||
{
|
||||
this.bot = bot;
|
||||
this.lastNP = new HashMap<>();
|
||||
}
|
||||
|
||||
public void init()
|
||||
{
|
||||
if(!bot.getConfig().useNPImages())
|
||||
bot.getThreadpool().scheduleWithFixedDelay(() -> updateAll(), 0, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void setLastNPMessage(Message m)
|
||||
{
|
||||
lastNP.put(m.getGuild().getIdLong(), new Pair<>(m.getChannel().getIdLong(), m.getIdLong()));
|
||||
}
|
||||
|
||||
public void clearLastNPMessage(Guild guild)
|
||||
{
|
||||
lastNP.remove(guild.getIdLong());
|
||||
}
|
||||
|
||||
private void updateAll()
|
||||
{
|
||||
Set<Long> toRemove = new HashSet<>();
|
||||
for(long guildId: lastNP.keySet())
|
||||
{
|
||||
Guild guild = bot.getJDA().getGuildById(guildId);
|
||||
if(guild==null)
|
||||
{
|
||||
toRemove.add(guildId);
|
||||
continue;
|
||||
}
|
||||
Pair<Long,Long> pair = lastNP.get(guildId);
|
||||
TextChannel tc = guild.getTextChannelById(pair.getKey());
|
||||
if(tc==null)
|
||||
{
|
||||
toRemove.add(guildId);
|
||||
continue;
|
||||
}
|
||||
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
|
||||
MessageCreateData msg = handler.getNowPlaying(bot.getJDA());
|
||||
if(msg==null)
|
||||
{
|
||||
msg = handler.getNoMusicPlaying(bot.getJDA());
|
||||
toRemove.add(guildId);
|
||||
}
|
||||
try
|
||||
{
|
||||
tc.editMessageById(pair.getValue(), MessageEditData.fromCreateData(msg)).queue(m->{}, t -> lastNP.remove(guildId));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
toRemove.add(guildId);
|
||||
}
|
||||
}
|
||||
toRemove.forEach(id -> lastNP.remove(id));
|
||||
}
|
||||
|
||||
// "event"-based methods
|
||||
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().inAudioChannel()).count()<=1)
|
||||
bot.getJDA().getPresence().setActivity(Activity.listening(track.getInfo().title));
|
||||
else
|
||||
bot.resetGame();
|
||||
}
|
||||
}
|
||||
|
||||
public void onMessageDelete(Guild guild, long messageId)
|
||||
{
|
||||
Pair<Long,Long> pair = lastNP.get(guild.getIdLong());
|
||||
if(pair==null)
|
||||
return;
|
||||
if(pair.getValue() == messageId)
|
||||
lastNP.remove(guild.getIdLong());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2018 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.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.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.Android;
|
||||
import dev.lavalink.youtube.clients.AndroidMusic;
|
||||
import dev.lavalink.youtube.clients.AndroidVr;
|
||||
import dev.lavalink.youtube.clients.ClientOptions;
|
||||
import dev.lavalink.youtube.clients.Ios;
|
||||
import dev.lavalink.youtube.clients.MWeb;
|
||||
import dev.lavalink.youtube.clients.Music;
|
||||
import dev.lavalink.youtube.clients.Tv;
|
||||
import dev.lavalink.youtube.clients.TvHtml5Simply;
|
||||
import dev.lavalink.youtube.clients.Web;
|
||||
import dev.lavalink.youtube.clients.WebEmbedded;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class PlayerManager extends DefaultAudioPlayerManager
|
||||
{
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(PlayerManager.class);
|
||||
private final 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));
|
||||
|
||||
YoutubeAudioSourceManager yt = setupYoutubeAudioSourceManager();
|
||||
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());
|
||||
try
|
||||
{
|
||||
registerSourceManager(new TwitchStreamAudioSourceManager());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.warn("Skipping Twitch source initialization because Twitch lookup failed during startup: {}", e.getMessage());
|
||||
LOGGER.debug("Twitch source initialization failure:", e);
|
||||
}
|
||||
registerSourceManager(new BeamAudioSourceManager());
|
||||
registerSourceManager(new GetyarnAudioSourceManager());
|
||||
registerSourceManager(new NicoAudioSourceManager());
|
||||
registerSourceManager(new HttpAudioSourceManager(MediaContainerRegistry.DEFAULT_REGISTRY));
|
||||
|
||||
AudioSourceManagers.registerLocalSource(this);
|
||||
|
||||
DuncteBotSources.registerAll(this, "en-US");
|
||||
}
|
||||
|
||||
private YoutubeAudioSourceManager setupYoutubeAudioSourceManager()
|
||||
{
|
||||
ClientOptions playbackOnly = new ClientOptions();
|
||||
playbackOnly.setSearching(false);
|
||||
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true,
|
||||
new Ios(playbackOnly),
|
||||
new Music(playbackOnly),
|
||||
new AndroidMusic(playbackOnly),
|
||||
new TvHtml5Simply(playbackOnly),
|
||||
new Tv(playbackOnly),
|
||||
new Web(),
|
||||
new MWeb(),
|
||||
new WebEmbedded(),
|
||||
new AndroidVr(playbackOnly),
|
||||
new Android(playbackOnly));
|
||||
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;
|
||||
}
|
||||
|
||||
public boolean hasHandler(Guild guild)
|
||||
{
|
||||
return guild.getAudioManager().getSendingHandler()!=null;
|
||||
}
|
||||
|
||||
public AudioHandler setUpHandler(Guild guild)
|
||||
{
|
||||
AudioHandler handler;
|
||||
if(guild.getAudioManager().getSendingHandler()==null)
|
||||
{
|
||||
AudioPlayer player = createPlayer();
|
||||
player.setVolume(bot.getSettingsManager().getSettings(guild).getVolume());
|
||||
handler = new AudioHandler(this, guild, player);
|
||||
player.addListener(handler);
|
||||
guild.getAudioManager().setSendingHandler(handler);
|
||||
}
|
||||
else
|
||||
handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2021 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.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 net.dv8tion.jda.api.entities.User;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class QueuedTrack implements Queueable
|
||||
{
|
||||
private final AudioTrack track;
|
||||
private final RequestMetadata requestMetadata;
|
||||
|
||||
public QueuedTrack(AudioTrack track, RequestMetadata rm)
|
||||
{
|
||||
this.track = track;
|
||||
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 requestMetadata.getOwner();
|
||||
}
|
||||
|
||||
public AudioTrack getTrack()
|
||||
{
|
||||
return track;
|
||||
}
|
||||
|
||||
public RequestMetadata getRequestMetadata()
|
||||
{
|
||||
return requestMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
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() + ">";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2021 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.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, null, null);
|
||||
|
||||
public final UserInfo user;
|
||||
public final RequestInfo requestInfo;
|
||||
public final QueueInfo queueInfo;
|
||||
|
||||
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 static class RequestInfo
|
||||
{
|
||||
public final String query, 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.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 static class UserInfo
|
||||
{
|
||||
public final long id;
|
||||
public final String username, discrim, avatar;
|
||||
|
||||
private UserInfo(long id, String username, String discrim, String avatar)
|
||||
{
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.discrim = discrim;
|
||||
this.avatar = 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2021 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.audio;
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
||||
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;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class TransformativeAudioSourceManager extends YoutubeAudioSourceManager
|
||||
{
|
||||
private final static Logger log = LoggerFactory.getLogger(TransformativeAudioSourceManager.class);
|
||||
private final String name, regex, replacement, selector, format;
|
||||
|
||||
public TransformativeAudioSourceManager(String name, Config object)
|
||||
{
|
||||
this(name, object.getString("regex"), object.getString("replacement"), object.getString("selector"), object.getString("format"));
|
||||
}
|
||||
|
||||
public TransformativeAudioSourceManager(String name, String regex, String replacement, String selector, String format)
|
||||
{
|
||||
this.name = name;
|
||||
this.regex = regex;
|
||||
this.replacement = replacement;
|
||||
this.selector = selector;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSourceName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioItem loadItem(AudioPlayerManager apm, AudioReference ar)
|
||||
{
|
||||
if(ar.identifier == null || !ar.identifier.matches(regex))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
String url = ar.identifier.replaceAll(regex, replacement);
|
||||
Document doc = Jsoup.connect(url).get();
|
||||
String value = doc.selectFirst(selector).ownText();
|
||||
String formattedValue = String.format(format, value);
|
||||
return super.loadItem(apm, new AudioReference(formattedValue, null));
|
||||
}
|
||||
catch (PatternSyntaxException ex)
|
||||
{
|
||||
log.info(String.format("Invalid pattern syntax '%s' in source '%s'", regex, name));
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
log.warn(String.format("Failed to resolve URL in source '%s': ", name), ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.warn(String.format("Exception in source '%s'", name), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<TransformativeAudioSourceManager> createTransforms(Config transforms)
|
||||
{
|
||||
try
|
||||
{
|
||||
return transforms.root().entrySet().stream()
|
||||
.map(e -> new TransformativeAudioSourceManager(e.getKey(), transforms.getConfig(e.getKey())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.warn("Invalid transform ", ex);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2018 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public abstract class AdminCommand extends Command
|
||||
{
|
||||
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()))
|
||||
return true;
|
||||
if(event.getGuild()==null)
|
||||
return true;
|
||||
return event.getMember().hasPermission(Permission.MANAGE_SERVER);
|
||||
});
|
||||
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,48 @@
|
||||
/*
|
||||
* Copyright 2018 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;
|
||||
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
import com.jagrosh.jmusicbot.settings.Settings;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public abstract class DJCommand extends MusicCommand
|
||||
{
|
||||
public DJCommand(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.category = new Category("DJ", event -> checkDJPermission(event));
|
||||
}
|
||||
|
||||
public static boolean checkDJPermission(CommandEvent event)
|
||||
{
|
||||
if(event.getAuthor().getId().equals(event.getClient().getOwnerId()))
|
||||
return true;
|
||||
if(event.getGuild()==null)
|
||||
return true;
|
||||
if(event.getMember().hasPermission(Permission.MANAGE_SERVER))
|
||||
return true;
|
||||
Settings settings = event.getClient().getSettingsFor(event.getGuild());
|
||||
Role dj = settings.getRole(event.getGuild());
|
||||
return dj!=null && (event.getMember().getRoles().contains(dj) || dj.getIdLong()==event.getGuild().getIdLong());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2018 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;
|
||||
|
||||
import com.jagrosh.jdautilities.command.Command;
|
||||
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.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.exceptions.PermissionException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public abstract class MusicCommand extends Command
|
||||
{
|
||||
protected final Bot bot;
|
||||
protected boolean bePlaying;
|
||||
protected boolean beListening;
|
||||
|
||||
public MusicCommand(Bot bot)
|
||||
{
|
||||
this.bot = bot;
|
||||
this.guildOnly = true;
|
||||
this.category = new Category("Music");
|
||||
}
|
||||
|
||||
@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))
|
||||
{
|
||||
try
|
||||
{
|
||||
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 = (VoiceChannel) event.getGuild().getSelfMember().getVoiceState().getChannel();
|
||||
if(current==null)
|
||||
current = settings.getVoiceChannel(event.getGuild());
|
||||
GuildVoiceState userState = event.getMember().getVoiceState();
|
||||
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;
|
||||
}
|
||||
|
||||
VoiceChannel afkChannel = userState.getGuild().getAfkChannel();
|
||||
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().inAudioChannel())
|
||||
{
|
||||
try
|
||||
{
|
||||
event.getGuild().getAudioManager().openAudioConnection(userState.getChannel());
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,74 @@
|
||||
/*
|
||||
* Copyright 2018 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public abstract class OwnerCommand extends Command
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2018 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.commands.AdminCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
import com.jagrosh.jmusicbot.settings.Settings;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2018 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 java.util.List;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2018 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 java.util.List;
|
||||
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.channel.concrete.TextChannel;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
{
|
||||
@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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2018 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 java.util.List;
|
||||
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.channel.concrete.VoiceChannel;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
{
|
||||
@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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2018 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.commands.AdminCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
import com.jagrosh.jmusicbot.settings.Settings;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
try
|
||||
{
|
||||
int val = Integer.parseInt(event.getArgs().endsWith("%") ? event.getArgs().substring(0,event.getArgs().length()-1) : event.getArgs());
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2019 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.dj;
|
||||
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Michaili K.
|
||||
*/
|
||||
public class ForceRemoveCmd extends DJCommand
|
||||
{
|
||||
public ForceRemoveCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "forceremove";
|
||||
this.help = "removes all entries by a user from the queue";
|
||||
this.arguments = "<user>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.beListening = false;
|
||||
this.bePlaying = true;
|
||||
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
if (event.getArgs().isEmpty())
|
||||
{
|
||||
event.replyError("You need to mention a user!");
|
||||
CommandLogContext.setError("missing_user");
|
||||
return;
|
||||
}
|
||||
|
||||
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
|
||||
if (handler.getQueue().isEmpty())
|
||||
{
|
||||
event.replyError("There is nothing in the queue!");
|
||||
CommandLogContext.setError("empty_queue");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
User target;
|
||||
List<Member> found = FinderUtil.findMembers(event.getArgs(), event.getGuild());
|
||||
|
||||
if(found.isEmpty())
|
||||
{
|
||||
event.replyError("Unable to find the user!");
|
||||
CommandLogContext.setError("user_not_found");
|
||||
return;
|
||||
}
|
||||
else if(found.size()>1)
|
||||
{
|
||||
OrderedMenu.Builder builder = new OrderedMenu.Builder();
|
||||
for(int i=0; i<found.size() && i<4; i++)
|
||||
{
|
||||
Member member = found.get(i);
|
||||
builder.addChoice("**"+member.getUser().getName()+"**#"+member.getUser().getDiscriminator());
|
||||
}
|
||||
|
||||
builder
|
||||
.setSelection((msg, i) -> removeAllEntries(found.get(i-1).getUser(), event))
|
||||
.setText("Found multiple users:")
|
||||
.setColor(event.getSelfMember().getColor())
|
||||
.useNumbers()
|
||||
.setUsers(event.getAuthor())
|
||||
.useCancelButton(true)
|
||||
.setCancel((msg) -> {})
|
||||
.setEventWaiter(bot.getWaiter())
|
||||
.setTimeout(1, TimeUnit.MINUTES)
|
||||
|
||||
.build().display(event.getChannel());
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = found.get(0).getUser();
|
||||
}
|
||||
|
||||
removeAllEntries(target, event);
|
||||
|
||||
}
|
||||
|
||||
private void removeAllEntries(User target, CommandEvent event)
|
||||
{
|
||||
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 "+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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2016 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.dj;
|
||||
|
||||
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.datalog.CommandLogContext;
|
||||
import com.jagrosh.jmusicbot.utils.FormatUtil;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class ForceskipCmd extends DJCommand
|
||||
{
|
||||
public ForceskipCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "forceskip";
|
||||
this.help = "skips the current song";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.bePlaying = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
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 **" + 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.jagrosh.jmusicbot.commands.dj;
|
||||
|
||||
|
||||
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.commands.DJCommand;
|
||||
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.
|
||||
*/
|
||||
public class MoveTrackCmd extends DJCommand
|
||||
{
|
||||
|
||||
public MoveTrackCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "movetrack";
|
||||
this.help = "move a track in the current queue to a different position";
|
||||
this.arguments = "<from> <to>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.bePlaying = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
int from;
|
||||
int to;
|
||||
|
||||
String[] parts = event.getArgs().split("\\s+", 2);
|
||||
if(parts.length < 2)
|
||||
{
|
||||
event.replyError("Please include two valid indexes.");
|
||||
CommandLogContext.setError("missing_indexes");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Validate the args
|
||||
from = Integer.parseInt(parts[0]);
|
||||
to = Integer.parseInt(parts[1]);
|
||||
}
|
||||
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();
|
||||
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(AbstractQueue<QueuedTrack> queue, int position)
|
||||
{
|
||||
return (position < 1 || position > queue.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2018 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.dj;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class PauseCmd extends DJCommand
|
||||
{
|
||||
public PauseCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "pause";
|
||||
this.help = "pauses the current song";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.bePlaying = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2018 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.dj;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class PlaynextCmd extends DJCommand
|
||||
{
|
||||
private final String loadingEmoji;
|
||||
|
||||
public PlaynextCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.loadingEmoji = bot.getConfig().getLoading();
|
||||
this.name = "playnext";
|
||||
this.arguments = "<title|URL>";
|
||||
this.help = "plays a single song next";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.beListening = true;
|
||||
this.bePlaying = false;
|
||||
}
|
||||
|
||||
@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(">")
|
||||
? event.getArgs().substring(1,event.getArgs().length()-1)
|
||||
: event.getArgs().isEmpty() ? event.getMessage().getAttachments().get(0).getUrl() : event.getArgs();
|
||||
event.reply(loadingEmoji+" Loading... `["+args+"]`", m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), args, new ResultHandler(m,event,false)));
|
||||
}
|
||||
|
||||
private class ResultHandler implements AudioLoadResultHandler
|
||||
{
|
||||
private final Message m;
|
||||
private final CommandEvent event;
|
||||
private final boolean ytsearch;
|
||||
|
||||
private ResultHandler(Message m, CommandEvent event, boolean ytsearch)
|
||||
{
|
||||
this.m = m;
|
||||
this.event = event;
|
||||
this.ytsearch = ytsearch;
|
||||
}
|
||||
|
||||
private void loadSingle(AudioTrack track)
|
||||
{
|
||||
if(bot.getConfig().isTooLong(track))
|
||||
{
|
||||
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
|
||||
+ 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();
|
||||
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
|
||||
+"** (`"+ 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
|
||||
public void trackLoaded(AudioTrack track)
|
||||
{
|
||||
loadSingle(track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist playlist)
|
||||
{
|
||||
AudioTrack single;
|
||||
if(playlist.getTracks().size()==1 || playlist.isSearchResult())
|
||||
single = playlist.getSelectedTrack()==null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
|
||||
else if (playlist.getSelectedTrack()!=null)
|
||||
single = playlist.getSelectedTrack();
|
||||
else
|
||||
single = playlist.getTracks().get(0);
|
||||
loadSingle(single);
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException throwable)
|
||||
{
|
||||
if(throwable.severity==FriendlyException.Severity.COMMON)
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2018 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class RepeatCmd extends DJCommand
|
||||
{
|
||||
public RepeatCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "repeat";
|
||||
this.help = "re-adds music to the queue when finished";
|
||||
this.arguments = "[off|all|single]";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
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)
|
||||
value = RepeatMode.ALL;
|
||||
else
|
||||
value = RepeatMode.OFF;
|
||||
}
|
||||
else if(args.equalsIgnoreCase("false") || args.equalsIgnoreCase("off"))
|
||||
{
|
||||
value = RepeatMode.OFF;
|
||||
}
|
||||
else if(args.equalsIgnoreCase("true") || args.equalsIgnoreCase("on") || args.equalsIgnoreCase("all"))
|
||||
{
|
||||
value = RepeatMode.ALL;
|
||||
}
|
||||
else if(args.equalsIgnoreCase("one") || args.equalsIgnoreCase("single"))
|
||||
{
|
||||
value = RepeatMode.SINGLE;
|
||||
}
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2016 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.dj;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class SkiptoCmd extends DJCommand
|
||||
{
|
||||
public SkiptoCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "skipto";
|
||||
this.help = "skips to the specified song";
|
||||
this.arguments = "<position>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.bePlaying = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
int index = 0;
|
||||
try
|
||||
{
|
||||
index = Integer.parseInt(event.getArgs());
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2018 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.dj;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class StopCmd extends DJCommand
|
||||
{
|
||||
public StopCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "stop";
|
||||
this.help = "stops the current song and clears the queue";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.bePlaying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2016 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.dj;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class VolumeCmd extends DJCommand
|
||||
{
|
||||
public VolumeCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "volume";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.help = "sets or shows volume";
|
||||
this.arguments = "[0-150]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
Settings settings = event.getClient().getSettingsFor(event.getGuild());
|
||||
int volume = handler.getPlayer().getVolume();
|
||||
if(event.getArgs().isEmpty())
|
||||
{
|
||||
event.reply(FormatUtil.volumeIcon(volume)+" Current volume is `"+volume+"`");
|
||||
CommandLogContext.setMeta(new JSONObject().put("current", volume));
|
||||
}
|
||||
else
|
||||
{
|
||||
int nvolume;
|
||||
try{
|
||||
nvolume = Integer.parseInt(event.getArgs());
|
||||
}catch(NumberFormatException e){
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2017 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.general;
|
||||
|
||||
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.utils.messages.MessageCreateBuilder;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
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 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);
|
||||
this.guildOnly = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
Settings s = event.getClient().getSettingsFor(event.getGuild());
|
||||
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())
|
||||
.setDescription("Text Channel: " + (tchan == null ? "Any" : "**#" + tchan.getName() + "**")
|
||||
+ "\nVoice Channel: " + (vchan == null ? "Any" : vchan.getAsMention())
|
||||
+ "\nDJ Role: " + (role == null ? "None" : "**" + role.getName() + "**")
|
||||
+ "\nCustom Prefix: " + (s.getPrefix() == null ? "None" : "`" + s.getPrefix() + "`")
|
||||
+ "\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().inAudioChannel()).count()
|
||||
+ " audio connections", null);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2018 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class LyricsCmd extends MusicCommand
|
||||
{
|
||||
private final LyricsClient client = new LyricsClient();
|
||||
|
||||
public LyricsCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "lyrics";
|
||||
this.arguments = "[song name]";
|
||||
this.help = "shows the lyrics of a song";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
CommandLogContext.setManualLogging();
|
||||
String title;
|
||||
if(event.getArgs().isEmpty())
|
||||
{
|
||||
AudioHandler sendingHandler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
|
||||
if (sendingHandler.isMusicPlaying(event.getJDA()))
|
||||
title = sendingHandler.getPlayer().getPlayingTrack().getInfo().title;
|
||||
else
|
||||
{
|
||||
event.replyError("There must be music playing to use that!");
|
||||
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_music_playing"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
title = event.getArgs();
|
||||
event.getChannel().sendTyping().queue();
|
||||
client.getLyrics(title).thenAccept(lyrics ->
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new EmbedBuilder()
|
||||
.setAuthor(lyrics.getAuthor())
|
||||
.setColor(event.getSelfMember().getColor())
|
||||
.setTitle(lyrics.getTitle(), lyrics.getURL());
|
||||
if(lyrics.getContent().length()>15000)
|
||||
{
|
||||
event.replyWarning("Lyrics for `" + title + "` found but likely not correct: " + lyrics.getURL());
|
||||
}
|
||||
else if(lyrics.getContent().length()>2000)
|
||||
{
|
||||
String content = lyrics.getContent().trim();
|
||||
while(content.length() > 2000)
|
||||
{
|
||||
int index = content.lastIndexOf("\n\n", 2000);
|
||||
if(index == -1)
|
||||
index = content.lastIndexOf("\n", 2000);
|
||||
if(index == -1)
|
||||
index = content.lastIndexOf(" ", 2000);
|
||||
if(index == -1)
|
||||
index = 2000;
|
||||
event.reply(eb.setDescription(content.substring(0, index).trim()).build());
|
||||
content = content.substring(index).trim();
|
||||
eb.setAuthor(null).setTitle(null, null);
|
||||
}
|
||||
event.reply(eb.setDescription(content).build());
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2018 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.commands.MusicCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class NowplayingCmd extends MusicCommand
|
||||
{
|
||||
public NowplayingCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "nowplaying";
|
||||
this.help = "shows the song that is currently playing";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
MessageCreateData m = handler.getNowPlaying(event.getJDA());
|
||||
if(m==null)
|
||||
{
|
||||
event.getMessage().getChannel().sendMessage(handler.getNoMusicPlaying(event.getJDA())).queue();
|
||||
bot.getNowplayingHandler().clearLastNPMessage(event.getGuild());
|
||||
CommandLogContext.setMeta(new JSONObject().put("playing", false));
|
||||
}
|
||||
else
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright 2016 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.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;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import com.jagrosh.jdautilities.command.Command;
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jdautilities.menu.ButtonMenu;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class PlayCmd extends MusicCommand
|
||||
{
|
||||
private final static String LOAD = "\uD83D\uDCE5"; // 📥
|
||||
private final static String CANCEL = "\uD83D\uDEAB"; // 🚫
|
||||
|
||||
private final String loadingEmoji;
|
||||
|
||||
public PlayCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.loadingEmoji = bot.getConfig().getLoading();
|
||||
this.name = "play";
|
||||
this.arguments = "<title|URL|subcommand>";
|
||||
this.help = "plays the provided song";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.beListening = true;
|
||||
this.bePlaying = false;
|
||||
this.children = new Command[]{new PlaylistCmd(bot)};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
CommandLogContext.setManualLogging();
|
||||
if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
|
||||
{
|
||||
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
if(handler.getPlayer().getPlayingTrack()!=null && handler.getPlayer().isPaused())
|
||||
{
|
||||
if(DJCommand.checkDJPermission(event))
|
||||
{
|
||||
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");
|
||||
builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" <song title>` - plays the first result from Youtube");
|
||||
builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" <URL>` - plays the provided song, playlist, or stream");
|
||||
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(">")
|
||||
? event.getArgs().substring(1,event.getArgs().length()-1)
|
||||
: event.getArgs().isEmpty() ? event.getMessage().getAttachments().get(0).getUrl() : event.getArgs();
|
||||
event.reply(loadingEmoji+" Loading... `["+args+"]`", m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), args, new ResultHandler(m,event,false)));
|
||||
}
|
||||
|
||||
private class ResultHandler implements AudioLoadResultHandler
|
||||
{
|
||||
private final Message m;
|
||||
private final CommandEvent event;
|
||||
private final boolean ytsearch;
|
||||
|
||||
private ResultHandler(Message m, CommandEvent event, boolean ytsearch)
|
||||
{
|
||||
this.m = m;
|
||||
this.event = event;
|
||||
this.ytsearch = ytsearch;
|
||||
}
|
||||
|
||||
private void loadSingle(AudioTrack track, AudioPlaylist playlist)
|
||||
{
|
||||
if(bot.getConfig().isTooLong(track))
|
||||
{
|
||||
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
|
||||
+ 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();
|
||||
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
|
||||
+"** (`"+ 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
|
||||
{
|
||||
new ButtonMenu.Builder()
|
||||
.setText(addMsg+"\n"+event.getClient().getWarning()+" This track has a playlist of **"+playlist.getTracks().size()+"** tracks attached. Select "+LOAD+" to load playlist.")
|
||||
.setChoices(LOAD, CANCEL)
|
||||
.setEventWaiter(bot.getWaiter())
|
||||
.setTimeout(30, TimeUnit.SECONDS)
|
||||
.setAction(re ->
|
||||
{
|
||||
if(re.getName().equals(LOAD))
|
||||
m.editMessage(addMsg+"\n"+event.getClient().getSuccess()+" Loaded **"+loadPlaylist(playlist, track)+"** additional tracks!").queue();
|
||||
else
|
||||
m.editMessage(addMsg).queue();
|
||||
}).setFinalAction(m ->
|
||||
{
|
||||
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)
|
||||
{
|
||||
int[] count = {0};
|
||||
playlist.getTracks().stream().forEach((track) -> {
|
||||
if(!bot.getConfig().isTooLong(track) && !track.equals(exclude))
|
||||
{
|
||||
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
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]++;
|
||||
}
|
||||
});
|
||||
return count[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack track)
|
||||
{
|
||||
loadSingle(track, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist playlist)
|
||||
{
|
||||
if(playlist.getTracks().size()==1 || playlist.isSearchResult())
|
||||
{
|
||||
AudioTrack single = playlist.getSelectedTrack()==null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
|
||||
loadSingle(single, null);
|
||||
}
|
||||
else if (playlist.getSelectedTrack()!=null)
|
||||
{
|
||||
AudioTrack single = playlist.getSelectedTrack();
|
||||
loadSingle(single, playlist);
|
||||
}
|
||||
else
|
||||
{
|
||||
int count = loadPlaylist(playlist, null);
|
||||
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
|
||||
{
|
||||
m.editMessage(FormatUtil.filter(event.getClient().getSuccess()+" Found "
|
||||
+(playlist.getName()==null?"a playlist":"playlist **"+playlist.getName()+"**")+" with `"
|
||||
+ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException throwable)
|
||||
{
|
||||
if(throwable.severity==Severity.COMMON)
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
public class PlaylistCmd extends MusicCommand
|
||||
{
|
||||
public PlaylistCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "playlist";
|
||||
this.aliases = new String[]{"pl"};
|
||||
this.arguments = "<name>";
|
||||
this.help = "plays the provided playlist";
|
||||
this.beListening = true;
|
||||
this.bePlaying = false;
|
||||
}
|
||||
|
||||
@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) ->
|
||||
{
|
||||
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!");
|
||||
if(!playlist.getErrors().isEmpty())
|
||||
builder.append("\nThe following tracks failed to load:");
|
||||
playlist.getErrors().forEach(err -> builder.append("\n`[").append(err.getIndex()+1).append("]` **").append(err.getItem()).append("**: ").append(err.getReason()));
|
||||
String str = builder.toString();
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2018 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 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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class PlaylistsCmd extends MusicCommand
|
||||
{
|
||||
public PlaylistsCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "playlists";
|
||||
this.help = "shows the available playlists";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = true;
|
||||
this.beListening = false;
|
||||
this.beListening = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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("` "));
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2018 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 java.util.List;
|
||||
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.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 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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class QueueCmd extends MusicCommand
|
||||
{
|
||||
private final Paginator.Builder builder;
|
||||
|
||||
public QueueCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "queue";
|
||||
this.help = "shows the current queue";
|
||||
this.arguments = "[pagenum]";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.bePlaying = true;
|
||||
this.botPermissions = new Permission[]{Permission.MESSAGE_ADD_REACTION,Permission.MESSAGE_EMBED_LINKS};
|
||||
builder = new Paginator.Builder()
|
||||
.setColumns(1)
|
||||
.setFinalAction(m -> {try{m.clearReactions().queue();}catch(PermissionException ignore){}})
|
||||
.setItemsPerPage(10)
|
||||
.waitOnSinglePage(false)
|
||||
.useNumberedItems(true)
|
||||
.showPageNumbers(true)
|
||||
.wrapPageEnds(true)
|
||||
.setEventWaiter(bot.getWaiter())
|
||||
.setTimeout(1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
int pagenum = 1;
|
||||
try
|
||||
{
|
||||
pagenum = Integer.parseInt(event.getArgs());
|
||||
}
|
||||
catch(NumberFormatException ignore){}
|
||||
AudioHandler ah = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
|
||||
List<QueuedTrack> list = ah.getQueue().getList();
|
||||
if(list.isEmpty())
|
||||
{
|
||||
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!")
|
||||
.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()];
|
||||
long total = 0;
|
||||
for(int i=0; i<list.size(); i++)
|
||||
{
|
||||
total += list.get(i).getTrack().getDuration();
|
||||
songs[i] = list.get(i).toString();
|
||||
}
|
||||
Settings settings = event.getClient().getSettingsFor(event.getGuild());
|
||||
long fintotal = total;
|
||||
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())
|
||||
;
|
||||
builder.build().paginate(event.getChannel(), pagenum);
|
||||
}
|
||||
|
||||
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.getStatusEmoji()).append(" **")
|
||||
.append(ah.getPlayer().getPlayingTrack().getInfo().title).append("**\n");
|
||||
}
|
||||
return FormatUtil.filter(sb.append(success).append(" Current Queue | ").append(songslength)
|
||||
.append(" entries | `").append(TimeUtil.formatTime(total)).append("` ")
|
||||
.append("| ").append(queueType.getEmoji()).append(" `").append(queueType.getUserFriendlyName()).append('`')
|
||||
.append(repeatmode.getEmoji() != null ? " | "+repeatmode.getEmoji() : "").toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2016 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class RemoveCmd extends MusicCommand
|
||||
{
|
||||
public RemoveCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "remove";
|
||||
this.help = "removes a song from the queue";
|
||||
this.arguments = "<position|ALL>";
|
||||
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();
|
||||
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;
|
||||
try {
|
||||
pos = Integer.parseInt(event.getArgs());
|
||||
} catch(NumberFormatException e) {
|
||||
pos = 0;
|
||||
}
|
||||
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());
|
||||
boolean isDJ = event.getMember().hasPermission(Permission.MANAGE_SERVER);
|
||||
if(!isDJ)
|
||||
isDJ = event.getMember().getRoles().contains(settings.getRole(event.getGuild()));
|
||||
QueuedTrack qt = handler.getQueue().get(pos-1);
|
||||
if(qt.getIdentifier()==event.getAuthor().getIdLong())
|
||||
{
|
||||
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)
|
||||
{
|
||||
handler.getQueue().remove(pos-1);
|
||||
User u;
|
||||
try {
|
||||
u = event.getJDA().getUserById(qt.getIdentifier());
|
||||
} catch(Exception e) {
|
||||
u = null;
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2016 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.jmusicbot.Bot;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class SCSearchCmd extends SearchCmd
|
||||
{
|
||||
public SCSearchCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.searchPrefix = "scsearch:";
|
||||
this.name = "scsearch";
|
||||
this.help = "searches Soundcloud for a provided query";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2016 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.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;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.jagrosh.jdautilities.command.CommandEvent;
|
||||
import com.jagrosh.jdautilities.menu.OrderedMenu;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class SearchCmd extends MusicCommand
|
||||
{
|
||||
protected String searchPrefix = "ytsearch:";
|
||||
private final OrderedMenu.Builder builder;
|
||||
private final String searchingEmoji;
|
||||
|
||||
public SearchCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.searchingEmoji = bot.getConfig().getSearching();
|
||||
this.name = "search";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.arguments = "<query>";
|
||||
this.help = "searches Youtube for a provided query";
|
||||
this.beListening = true;
|
||||
this.bePlaying = false;
|
||||
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
|
||||
builder = new OrderedMenu.Builder()
|
||||
.allowTextInput(true)
|
||||
.useNumbers()
|
||||
.useCancelButton(true)
|
||||
.setEventWaiter(bot.getWaiter())
|
||||
.setTimeout(1, TimeUnit.MINUTES);
|
||||
}
|
||||
@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()+"]`",
|
||||
m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), searchPrefix + event.getArgs(), new ResultHandler(m,event)));
|
||||
}
|
||||
|
||||
private class ResultHandler implements AudioLoadResultHandler
|
||||
{
|
||||
private final Message m;
|
||||
private final CommandEvent event;
|
||||
|
||||
private ResultHandler(Message m, CommandEvent event)
|
||||
{
|
||||
this.m = m;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack track)
|
||||
{
|
||||
if(bot.getConfig().isTooLong(track))
|
||||
{
|
||||
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
|
||||
+ 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();
|
||||
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
|
||||
+"** (`"+ 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
|
||||
public void playlistLoaded(AudioPlaylist playlist)
|
||||
{
|
||||
builder.setColor(event.getSelfMember().getColor())
|
||||
.setText(FormatUtil.filter(event.getClient().getSuccess()+" Search results for `"+event.getArgs()+"`:"))
|
||||
.setChoices(new String[0])
|
||||
.setSelection((msg,i) ->
|
||||
{
|
||||
AudioTrack track = playlist.getTracks().get(i-1);
|
||||
if(bot.getConfig().isTooLong(track))
|
||||
{
|
||||
event.replyWarning("This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
|
||||
+ 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();
|
||||
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)
|
||||
+ "** (`" + TimeUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing"
|
||||
: " to the queue at position "+pos));
|
||||
})
|
||||
.setCancel((msg) -> {})
|
||||
.setUsers(event.getAuthor())
|
||||
;
|
||||
for(int i=0; i<4 && i<playlist.getTracks().size(); i++)
|
||||
{
|
||||
AudioTrack track = playlist.getTracks().get(i);
|
||||
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
|
||||
public void loadFailed(FriendlyException throwable)
|
||||
{
|
||||
if(throwable.severity==Severity.COMMON)
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2018 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.commands.MusicCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class ShuffleCmd extends MusicCommand
|
||||
{
|
||||
public ShuffleCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "shuffle";
|
||||
this.help = "shuffles songs you have added";
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2016 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.MusicCommand;
|
||||
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
|
||||
import com.jagrosh.jmusicbot.utils.FormatUtil;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class SkipCmd extends MusicCommand
|
||||
{
|
||||
public SkipCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "skip";
|
||||
this.help = "votes to skip the current song";
|
||||
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();
|
||||
RequestMetadata rm = handler.getRequestMetadata();
|
||||
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
|
||||
{
|
||||
int listeners = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream()
|
||||
.filter(m -> !m.getUser().isBot() && !m.getVoiceState().isDeafened()).count();
|
||||
String msg;
|
||||
if(handler.getVotes().contains(event.getAuthor().getId()))
|
||||
msg = event.getClient().getWarning()+" You already voted to skip this song `[";
|
||||
else
|
||||
{
|
||||
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 * 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 **" + 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2016 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class AutoplaylistCmd extends OwnerCommand
|
||||
{
|
||||
public AutoplaylistCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.guildOnly = true;
|
||||
this.name = "autoplaylist";
|
||||
this.arguments = "<name|NONE>";
|
||||
this.help = "sets the default playlist for the server";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
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.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.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2017 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.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.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.channel.ChannelType;
|
||||
import net.dv8tion.jda.api.utils.FileUpload;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
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"};
|
||||
|
||||
public DebugCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "debug";
|
||||
this.help = "shows debug info";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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)
|
||||
sb.append("\n ").append(key).append(" = ").append(System.getProperty(key));
|
||||
sb.append("\n\nJMusicBot Information:")
|
||||
.append("\n Version = ").append(OtherUtil.getCurrentVersion())
|
||||
.append("\n Owner = ").append(bot.getConfig().getOwnerId())
|
||||
.append("\n Prefix = ").append(bot.getConfig().getPrefix())
|
||||
.append("\n AltPrefix = ").append(bot.getConfig().getAltPrefix())
|
||||
.append("\n MaxSeconds = ").append(bot.getConfig().getMaxSeconds())
|
||||
.append("\n NPImages = ").append(bot.getConfig().useNPImages())
|
||||
.append("\n SongInStatus = ").append(bot.getConfig().getSongInStatus())
|
||||
.append("\n StayInChannel = ").append(bot.getConfig().getStay())
|
||||
.append("\n UseEval = ").append(bot.getConfig().useEval())
|
||||
.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(OtherUtil.getCommandLibraryVersion())
|
||||
.append("\n Lavaplayer Version = ").append(PlayerLibrary.VERSION);
|
||||
long total = Runtime.getRuntime().totalMemory() / 1024 / 1024;
|
||||
long used = total - (Runtime.getRuntime().freeMemory() / 1024 / 1024);
|
||||
sb.append("\n\nRuntime Information:")
|
||||
.append("\n Total Memory = ").append(total)
|
||||
.append("\n Used Memory = ").append(used);
|
||||
sb.append("\n\nDiscord Information:")
|
||||
.append("\n ID = ").append(event.getJDA().getSelfUser().getId())
|
||||
.append("\n Guilds = ").append(event.getJDA().getGuildCache().size())
|
||||
.append("\n Users = ").append(event.getJDA().getUserCache().size());
|
||||
sb.append("\n```");
|
||||
|
||||
if(event.isFromType(ChannelType.PRIVATE)
|
||||
|| 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2016 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.commands.owner;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (jagrosh)
|
||||
*/
|
||||
public class EvalCmd extends OwnerCommand
|
||||
{
|
||||
private final String engine;
|
||||
|
||||
public EvalCmd(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
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
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());
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2016 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.owner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class PlaylistCmd extends OwnerCommand
|
||||
{
|
||||
public PlaylistCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.guildOnly = false;
|
||||
this.name = "playlist";
|
||||
this.arguments = "<append|delete|make|setdefault>";
|
||||
this.help = "playlist management";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.children = new OwnerCommand[]{
|
||||
new ListCmd(),
|
||||
new AppendlistCmd(),
|
||||
new DeletelistCmd(),
|
||||
new MakelistCmd(),
|
||||
new DefaultlistCmd(bot)
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Playlist Management Commands:\n");
|
||||
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";
|
||||
this.arguments = "<name>";
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
String pname = event.getArgs().replaceAll("\\s+", "_");
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DeletelistCmd extends OwnerCommand
|
||||
{
|
||||
public DeletelistCmd()
|
||||
{
|
||||
super(PlaylistCmd.this.bot);
|
||||
this.name = "delete";
|
||||
this.aliases = new String[]{"remove"};
|
||||
this.help = "deletes an existing playlist";
|
||||
this.arguments = "<name>";
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AppendlistCmd extends OwnerCommand
|
||||
{
|
||||
public AppendlistCmd()
|
||||
{
|
||||
super(PlaylistCmd.this.bot);
|
||||
this.name = "append";
|
||||
this.aliases = new String[]{"add"};
|
||||
this.help = "appends songs to an existing playlist";
|
||||
this.arguments = "<name> <URL> | <URL> | ...";
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
playlist.getItems().forEach(item -> builder.append("\r\n").append(item));
|
||||
String[] urls = parts[1].split("\\|");
|
||||
for(String url: urls)
|
||||
{
|
||||
String u = url.trim();
|
||||
if(u.startsWith("<") && u.endsWith(">"))
|
||||
u = u.substring(1, u.length()-1);
|
||||
builder.append("\r\n").append(u);
|
||||
}
|
||||
try
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultlistCmd extends AutoplaylistCmd
|
||||
{
|
||||
public DefaultlistCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "setdefault";
|
||||
this.aliases = new String[]{"default"};
|
||||
this.arguments = "<playlistname|NONE>";
|
||||
this.guildOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ListCmd extends OwnerCommand
|
||||
{
|
||||
public ListCmd()
|
||||
{
|
||||
super(PlaylistCmd.this.bot);
|
||||
this.name = "all";
|
||||
this.aliases = new String[]{"available","list"};
|
||||
this.help = "lists all available playlists";
|
||||
this.guildOnly = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2017 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.owner;
|
||||
|
||||
import java.io.IOException;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
CommandLogContext.setManualLogging();
|
||||
String url;
|
||||
if(event.getArgs().isEmpty())
|
||||
if(!event.getMessage().getAttachments().isEmpty() && event.getMessage().getAttachments().get(0).isImage())
|
||||
url = event.getMessage().getAttachments().get(0).getUrl();
|
||||
else
|
||||
url = null;
|
||||
else
|
||||
url = event.getArgs();
|
||||
InputStream s = OtherUtil.imageFromUrl(url);
|
||||
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.");
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2017 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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]";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = false;
|
||||
this.children = new OwnerCommand[]{
|
||||
new SetlistenCmd(),
|
||||
new SetstreamCmd(),
|
||||
new SetwatchCmd()
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doCommand(CommandEvent event)
|
||||
{
|
||||
String title = event.getArgs().toLowerCase().startsWith("playing") ? event.getArgs().substring(7).trim() : event.getArgs();
|
||||
try
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
private class SetstreamCmd 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";
|
||||
this.arguments = "<username> <game>";
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SetlistenCmd 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";
|
||||
this.arguments = "<title>";
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
try
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SetwatchCmd 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";
|
||||
this.arguments = "<title>";
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
try
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2018 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2017 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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>";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2017 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.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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class ShutdownCmd extends OwnerCommand
|
||||
{
|
||||
public ShutdownCmd(Bot bot)
|
||||
{
|
||||
super(bot);
|
||||
this.name = "shutdown";
|
||||
this.help = "safely shuts down";
|
||||
this.aliases = bot.getConfig().getAliases(this.name);
|
||||
this.guildOnly = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2018 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.entities;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*/
|
||||
public class Pair<K,V>
|
||||
{
|
||||
private final K key;
|
||||
private final V value;
|
||||
|
||||
public Pair(K key, V value)
|
||||
{
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public K getKey()
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
public V getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2018 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.entities;
|
||||
|
||||
import java.util.Scanner;
|
||||
import javax.swing.JOptionPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class Prompt
|
||||
{
|
||||
private final String title;
|
||||
private final String noguiMessage;
|
||||
|
||||
private boolean nogui;
|
||||
private boolean noprompt;
|
||||
private Scanner scanner;
|
||||
|
||||
public Prompt(String title)
|
||||
{
|
||||
this(title, null);
|
||||
}
|
||||
|
||||
public Prompt(String title, String noguiMessage)
|
||||
{
|
||||
this(title, noguiMessage, "true".equalsIgnoreCase(System.getProperty("nogui")), "true".equalsIgnoreCase(System.getProperty("noprompt")));
|
||||
}
|
||||
|
||||
public Prompt(String title, String noguiMessage, boolean nogui, boolean noprompt)
|
||||
{
|
||||
this.title = title;
|
||||
this.noguiMessage = noguiMessage == null ? "Switching to nogui mode. You can manually start in nogui mode by including the -Dnogui=true flag." : noguiMessage;
|
||||
this.nogui = nogui;
|
||||
this.noprompt = noprompt;
|
||||
}
|
||||
|
||||
public boolean isNoGUI()
|
||||
{
|
||||
return nogui;
|
||||
}
|
||||
|
||||
public void alert(Level level, String context, String message)
|
||||
{
|
||||
if(nogui)
|
||||
{
|
||||
Logger log = LoggerFactory.getLogger(context);
|
||||
switch(level)
|
||||
{
|
||||
case INFO:
|
||||
log.info(message);
|
||||
break;
|
||||
case WARNING:
|
||||
log.warn(message);
|
||||
break;
|
||||
case ERROR:
|
||||
log.error(message);
|
||||
break;
|
||||
default:
|
||||
log.info(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
int option = 0;
|
||||
switch(level)
|
||||
{
|
||||
case INFO:
|
||||
option = JOptionPane.INFORMATION_MESSAGE;
|
||||
break;
|
||||
case WARNING:
|
||||
option = JOptionPane.WARNING_MESSAGE;
|
||||
break;
|
||||
case ERROR:
|
||||
option = JOptionPane.ERROR_MESSAGE;
|
||||
break;
|
||||
default:
|
||||
option = JOptionPane.PLAIN_MESSAGE;
|
||||
break;
|
||||
}
|
||||
JOptionPane.showMessageDialog(null, "<html><body><p style='width: 400px;'>"+message, title, option);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
nogui = true;
|
||||
alert(Level.WARNING, context, noguiMessage);
|
||||
alert(level, context, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String prompt(String content)
|
||||
{
|
||||
if(noprompt)
|
||||
return null;
|
||||
if(nogui)
|
||||
{
|
||||
if(scanner==null)
|
||||
scanner = new Scanner(System.in);
|
||||
try
|
||||
{
|
||||
System.out.println(content);
|
||||
if(scanner.hasNextLine())
|
||||
return scanner.nextLine();
|
||||
return null;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
alert(Level.ERROR, title, "Unable to read input from command line.");
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
return JOptionPane.showInputDialog(null, content, title, JOptionPane.QUESTION_MESSAGE);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
nogui = true;
|
||||
alert(Level.WARNING, title, noguiMessage);
|
||||
return prompt(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static enum Level
|
||||
{
|
||||
INFO, WARNING, ERROR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2017 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.gui;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GridLayout;
|
||||
import java.io.PrintStream;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextArea;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class ConsolePanel extends JPanel {
|
||||
|
||||
public ConsolePanel()
|
||||
{
|
||||
super();
|
||||
JTextArea text = new JTextArea();
|
||||
text.setLineWrap(true);
|
||||
text.setWrapStyleWord(true);
|
||||
text.setEditable(false);
|
||||
PrintStream con=new PrintStream(new TextAreaOutputStream(text));
|
||||
System.setOut(con);
|
||||
System.setErr(con);
|
||||
|
||||
JScrollPane pane = new JScrollPane();
|
||||
pane.setViewportView(text);
|
||||
|
||||
super.setLayout(new GridLayout(1,1));
|
||||
super.add(pane);
|
||||
super.setPreferredSize(new Dimension(400,300));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2016 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.gui;
|
||||
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowListener;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JTabbedPane;
|
||||
import javax.swing.WindowConstants;
|
||||
import com.jagrosh.jmusicbot.Bot;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class GUI extends JFrame
|
||||
{
|
||||
private final ConsolePanel console;
|
||||
private final Bot bot;
|
||||
|
||||
public GUI(Bot bot)
|
||||
{
|
||||
super();
|
||||
this.bot = bot;
|
||||
console = new ConsolePanel();
|
||||
}
|
||||
|
||||
public void init()
|
||||
{
|
||||
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
|
||||
setTitle("JMusicBot");
|
||||
JTabbedPane tabs = new JTabbedPane();
|
||||
tabs.add("Console", console);
|
||||
getContentPane().add(tabs);
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setVisible(true);
|
||||
addWindowListener(new WindowListener()
|
||||
{
|
||||
@Override public void windowOpened(WindowEvent e) { /* unused */ }
|
||||
@Override public void windowClosing(WindowEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
bot.shutdown();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@Override public void windowClosed(WindowEvent e) { /* unused */ }
|
||||
@Override public void windowIconified(WindowEvent e) { /* unused */ }
|
||||
@Override public void windowDeiconified(WindowEvent e) { /* unused */ }
|
||||
@Override public void windowActivated(WindowEvent e) { /* unused */ }
|
||||
@Override public void windowDeactivated(WindowEvent e) { /* unused */ }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2017 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.gui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import javax.swing.*;
|
||||
/**
|
||||
*
|
||||
* @author Lawrence Dol
|
||||
*/
|
||||
public class TextAreaOutputStream extends OutputStream {
|
||||
|
||||
// *************************************************************************************************
|
||||
// INSTANCE MEMBERS
|
||||
// *************************************************************************************************
|
||||
|
||||
private byte[] oneByte; // array for write(int val);
|
||||
private Appender appender; // most recent action
|
||||
|
||||
public TextAreaOutputStream(JTextArea txtara) {
|
||||
this(txtara,1000);
|
||||
}
|
||||
|
||||
public TextAreaOutputStream(JTextArea txtara, int maxlin) {
|
||||
if(maxlin<1) { throw new IllegalArgumentException("TextAreaOutputStream maximum lines must be positive (value="+maxlin+")"); }
|
||||
oneByte=new byte[1];
|
||||
appender=new Appender(txtara,maxlin);
|
||||
}
|
||||
|
||||
/** Clear the current console text area. */
|
||||
public synchronized void clear() {
|
||||
if(appender!=null) { appender.clear(); }
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
appender=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void flush() {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int val) {
|
||||
oneByte[0]=(byte)val;
|
||||
write(oneByte,0,1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] ba) {
|
||||
write(ba,0,ba.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] ba,int str,int len) {
|
||||
if(appender!=null) { appender.append(bytesToString(ba,str,len)); }
|
||||
}
|
||||
|
||||
//@edu.umd.cs.findbugs.annotations.SuppressWarnings("DM_DEFAULT_ENCODING")
|
||||
static private String bytesToString(byte[] ba, int str, int len) {
|
||||
try {
|
||||
return new String(ba,str,len,"UTF-8");
|
||||
} catch(UnsupportedEncodingException thr) {
|
||||
return new String(ba,str,len);
|
||||
} // all JVMs are required to support UTF-8
|
||||
}
|
||||
|
||||
// *************************************************************************************************
|
||||
// STATIC MEMBERS
|
||||
// *************************************************************************************************
|
||||
|
||||
static class Appender
|
||||
implements Runnable
|
||||
{
|
||||
static private final String EOL1="\n";
|
||||
static private final String EOL2=System.getProperty("line.separator",EOL1);
|
||||
|
||||
private final JTextArea textArea;
|
||||
private final int maxLines; // maximum lines allowed in text area
|
||||
private final LinkedList<Integer> lengths; // length of lines within text area
|
||||
private final List<String> values; // values waiting to be appended
|
||||
|
||||
private int curLength; // length of current line
|
||||
private boolean clear;
|
||||
private boolean queue;
|
||||
|
||||
Appender(JTextArea txtara, int maxlin) {
|
||||
textArea =txtara;
|
||||
maxLines =maxlin;
|
||||
lengths =new LinkedList<>();
|
||||
values =new ArrayList<>();
|
||||
|
||||
curLength=0;
|
||||
clear =false;
|
||||
queue =true;
|
||||
}
|
||||
|
||||
private synchronized void append(String val) {
|
||||
values.add(val);
|
||||
if(queue) {
|
||||
queue=false;
|
||||
EventQueue.invokeLater(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void clear() {
|
||||
clear=true;
|
||||
curLength=0;
|
||||
lengths.clear();
|
||||
values.clear();
|
||||
if(queue) {
|
||||
queue=false;
|
||||
EventQueue.invokeLater(this);
|
||||
}
|
||||
}
|
||||
|
||||
// MUST BE THE ONLY METHOD THAT TOUCHES textArea!
|
||||
@Override
|
||||
public synchronized void run() {
|
||||
if(clear) { textArea.setText(""); }
|
||||
values.stream().map((val) -> {
|
||||
curLength+=val.length();
|
||||
return val;
|
||||
}).map((val) -> {
|
||||
if(val.endsWith(EOL1) || val.endsWith(EOL2)) {
|
||||
if(lengths.size()>=maxLines) { textArea.replaceRange("",0,lengths.removeFirst()); }
|
||||
lengths.addLast(curLength);
|
||||
curLength=0;
|
||||
}
|
||||
return val;
|
||||
}).forEach((val) -> {
|
||||
textArea.append(val);
|
||||
});
|
||||
values.clear();
|
||||
clear =false;
|
||||
queue =true;
|
||||
}
|
||||
}
|
||||
} /* END PUBLIC CLASS */
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright 2018 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.playlist;
|
||||
|
||||
import com.jagrosh.jmusicbot.BotConfig;
|
||||
import com.jagrosh.jmusicbot.utils.OtherUtil;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (john.a.grosh@gmail.com)
|
||||
*/
|
||||
public class PlaylistLoader
|
||||
{
|
||||
private final BotConfig config;
|
||||
|
||||
public PlaylistLoader(BotConfig config)
|
||||
{
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public List<String> getPlaylistNames()
|
||||
{
|
||||
if(folderExists())
|
||||
{
|
||||
File folder = new File(OtherUtil.getPath(config.getPlaylistsFolder()).toString());
|
||||
return Arrays.asList(folder.listFiles((pathname) -> pathname.getName().endsWith(".txt")))
|
||||
.stream().map(f -> f.getName().substring(0,f.getName().length()-4)).collect(Collectors.toList());
|
||||
}
|
||||
else
|
||||
{
|
||||
createFolder();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public void createFolder()
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.createDirectory(OtherUtil.getPath(config.getPlaylistsFolder()));
|
||||
}
|
||||
catch (IOException ignore) {}
|
||||
}
|
||||
|
||||
public boolean folderExists()
|
||||
{
|
||||
return Files.exists(OtherUtil.getPath(config.getPlaylistsFolder()));
|
||||
}
|
||||
|
||||
public void createPlaylist(String name) throws IOException
|
||||
{
|
||||
Files.createFile(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"));
|
||||
}
|
||||
|
||||
public void deletePlaylist(String name) throws IOException
|
||||
{
|
||||
Files.delete(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"));
|
||||
}
|
||||
|
||||
public void writePlaylist(String name, String text) throws IOException
|
||||
{
|
||||
Files.write(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"), text.trim().getBytes());
|
||||
}
|
||||
|
||||
public Playlist getPlaylist(String name)
|
||||
{
|
||||
if(!getPlaylistNames().contains(name))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
if(folderExists())
|
||||
{
|
||||
boolean[] shuffle = {false};
|
||||
List<String> list = new ArrayList<>();
|
||||
Files.readAllLines(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt")).forEach(str ->
|
||||
{
|
||||
String s = str.trim();
|
||||
if(s.isEmpty())
|
||||
return;
|
||||
if(s.startsWith("#") || s.startsWith("//"))
|
||||
{
|
||||
s = s.replaceAll("\\s+", "");
|
||||
if(s.equalsIgnoreCase("#shuffle") || s.equalsIgnoreCase("//shuffle"))
|
||||
shuffle[0]=true;
|
||||
}
|
||||
else
|
||||
list.add(s);
|
||||
});
|
||||
if(shuffle[0])
|
||||
shuffle(list);
|
||||
return new Playlist(name, list, shuffle[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
createFolder();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static <T> void shuffle(List<T> list)
|
||||
{
|
||||
for(int first =0; first<list.size(); first++)
|
||||
{
|
||||
int second = (int)(Math.random()*list.size());
|
||||
T tmp = list.get(first);
|
||||
list.set(first, list.get(second));
|
||||
list.set(second, tmp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Playlist
|
||||
{
|
||||
private final String name;
|
||||
private final List<String> items;
|
||||
private final boolean shuffle;
|
||||
private final List<AudioTrack> tracks = new LinkedList<>();
|
||||
private final List<PlaylistLoadError> errors = new LinkedList<>();
|
||||
private boolean loaded = false;
|
||||
|
||||
private Playlist(String name, List<String> items, boolean shuffle)
|
||||
{
|
||||
this.name = name;
|
||||
this.items = items;
|
||||
this.shuffle = shuffle;
|
||||
}
|
||||
|
||||
public void loadTracks(AudioPlayerManager manager, Consumer<AudioTrack> consumer, Runnable callback)
|
||||
{
|
||||
if(loaded)
|
||||
return;
|
||||
loaded = true;
|
||||
for(int i=0; i<items.size(); i++)
|
||||
{
|
||||
boolean last = i+1 == items.size();
|
||||
int index = i;
|
||||
manager.loadItemOrdered(name, items.get(i), new AudioLoadResultHandler()
|
||||
{
|
||||
private void done()
|
||||
{
|
||||
if(last)
|
||||
{
|
||||
if(shuffle)
|
||||
shuffleTracks();
|
||||
if(callback != null)
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack at)
|
||||
{
|
||||
if(config.isTooLong(at))
|
||||
errors.add(new PlaylistLoadError(index, items.get(index), "This track is longer than the allowed maximum"));
|
||||
else
|
||||
{
|
||||
at.setUserData(0L);
|
||||
tracks.add(at);
|
||||
consumer.accept(at);
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist ap)
|
||||
{
|
||||
if(ap.isSearchResult())
|
||||
{
|
||||
trackLoaded(ap.getTracks().get(0));
|
||||
}
|
||||
else if(ap.getSelectedTrack()!=null)
|
||||
{
|
||||
trackLoaded(ap.getSelectedTrack());
|
||||
}
|
||||
else
|
||||
{
|
||||
List<AudioTrack> loaded = new ArrayList<>(ap.getTracks());
|
||||
if(shuffle)
|
||||
for(int first =0; first<loaded.size(); first++)
|
||||
{
|
||||
int second = (int)(Math.random()*loaded.size());
|
||||
AudioTrack tmp = loaded.get(first);
|
||||
loaded.set(first, loaded.get(second));
|
||||
loaded.set(second, tmp);
|
||||
}
|
||||
loaded.removeIf(track -> config.isTooLong(track));
|
||||
loaded.forEach(at -> at.setUserData(0L));
|
||||
tracks.addAll(loaded);
|
||||
loaded.forEach(at -> consumer.accept(at));
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches()
|
||||
{
|
||||
errors.add(new PlaylistLoadError(index, items.get(index), "No matches found."));
|
||||
done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException fe)
|
||||
{
|
||||
errors.add(new PlaylistLoadError(index, items.get(index), "Failed to load track: "+fe.getLocalizedMessage()));
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void shuffleTracks()
|
||||
{
|
||||
shuffle(tracks);
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> getItems()
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
public List<AudioTrack> getTracks()
|
||||
{
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public List<PlaylistLoadError> getErrors()
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
public class PlaylistLoadError
|
||||
{
|
||||
private final int number;
|
||||
private final String item;
|
||||
private final String reason;
|
||||
|
||||
private PlaylistLoadError(int number, String item, String reason)
|
||||
{
|
||||
this.number = number;
|
||||
this.item = item;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public int getIndex()
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getItem()
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
public String getReason()
|
||||
{
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2016 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.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (jagrosh)
|
||||
* @param <T>
|
||||
*/
|
||||
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())
|
||||
break;
|
||||
lastIndex++;
|
||||
set.clear();
|
||||
for(; lastIndex<list.size(); lastIndex++)
|
||||
{
|
||||
if(set.contains(list.get(lastIndex).getIdentifier()))
|
||||
break;
|
||||
set.add(list.get(lastIndex).getIdentifier());
|
||||
}
|
||||
list.add(lastIndex, item);
|
||||
return lastIndex;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+25
-25
@@ -1,25 +1,25 @@
|
||||
|
||||
/*
|
||||
* Copyright 2016 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 spectramusic;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh (jagrosh)
|
||||
*/
|
||||
public class GUI {
|
||||
|
||||
}
|
||||
/*
|
||||
* Copyright 2016 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 John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public interface Queueable {
|
||||
|
||||
public long getIdentifier();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.settings;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Michaili K
|
||||
*/
|
||||
public enum RepeatMode
|
||||
{
|
||||
OFF(null, "Off"),
|
||||
ALL("\uD83D\uDD01", "All"), // 🔁
|
||||
SINGLE("\uD83D\uDD02", "Single"); // 🔂
|
||||
|
||||
private final String emoji;
|
||||
private final String userFriendlyName;
|
||||
|
||||
private RepeatMode(String emoji, String userFriendlyName)
|
||||
{
|
||||
this.emoji = emoji;
|
||||
this.userFriendlyName = userFriendlyName;
|
||||
}
|
||||
|
||||
public String getEmoji()
|
||||
{
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public String getUserFriendlyName()
|
||||
{
|
||||
return userFriendlyName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2016 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.jdautilities.command.GuildSettingsProvider;
|
||||
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.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
public class Settings implements GuildSettingsProvider
|
||||
{
|
||||
private final SettingsManager manager;
|
||||
protected long textId;
|
||||
protected long voiceId;
|
||||
protected long roleId;
|
||||
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, 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
|
||||
{
|
||||
this.textId = Long.parseLong(textId);
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
this.textId = 0;
|
||||
}
|
||||
try
|
||||
{
|
||||
this.voiceId = Long.parseLong(voiceId);
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
this.voiceId = 0;
|
||||
}
|
||||
try
|
||||
{
|
||||
this.roleId = Long.parseLong(roleId);
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
this.roleId = 0;
|
||||
}
|
||||
this.volume = volume;
|
||||
this.defaultPlaylist = defaultPlaylist;
|
||||
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, 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;
|
||||
this.voiceId = voiceId;
|
||||
this.roleId = roleId;
|
||||
this.volume = volume;
|
||||
this.defaultPlaylist = defaultPlaylist;
|
||||
this.repeatMode = repeatMode;
|
||||
this.prefix = prefix;
|
||||
this.skipRatio = skipRatio;
|
||||
this.queueType = queueType;
|
||||
this.statsReportChannelId = statsReportChannelId;
|
||||
this.statsReportFrequency = statsReportFrequency == null ? StatsReportFrequency.OFF : statsReportFrequency;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public TextChannel getTextChannel(Guild guild)
|
||||
{
|
||||
return guild == null ? null : guild.getTextChannelById(textId);
|
||||
}
|
||||
|
||||
public VoiceChannel getVoiceChannel(Guild guild)
|
||||
{
|
||||
return guild == null ? null : guild.getVoiceChannelById(voiceId);
|
||||
}
|
||||
|
||||
public Role getRole(Guild guild)
|
||||
{
|
||||
return guild == null ? null : guild.getRoleById(roleId);
|
||||
}
|
||||
|
||||
public int getVolume()
|
||||
{
|
||||
return volume;
|
||||
}
|
||||
|
||||
public String getDefaultPlaylist()
|
||||
{
|
||||
return defaultPlaylist;
|
||||
}
|
||||
|
||||
public RepeatMode getRepeatMode()
|
||||
{
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
public String getPrefix()
|
||||
{
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public double getSkipRatio()
|
||||
{
|
||||
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.emptySet() : Collections.singleton(prefix);
|
||||
}
|
||||
|
||||
// Setters
|
||||
public void setTextChannel(TextChannel tc)
|
||||
{
|
||||
this.textId = tc == null ? 0 : tc.getIdLong();
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setVoiceChannel(VoiceChannel vc)
|
||||
{
|
||||
this.voiceId = vc == null ? 0 : vc.getIdLong();
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setDJRole(Role role)
|
||||
{
|
||||
this.roleId = role == null ? 0 : role.getIdLong();
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setVolume(int volume)
|
||||
{
|
||||
this.volume = volume;
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setDefaultPlaylist(String defaultPlaylist)
|
||||
{
|
||||
this.defaultPlaylist = defaultPlaylist;
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setRepeatMode(RepeatMode mode)
|
||||
{
|
||||
this.repeatMode = mode;
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix)
|
||||
{
|
||||
this.prefix = prefix;
|
||||
this.manager.writeSettings();
|
||||
}
|
||||
|
||||
public void setSkipRatio(double skipRatio)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2018 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.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<Settings>
|
||||
{
|
||||
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(SETTINGS_FILE))));
|
||||
loadedSettings.keySet().forEach((id) -> {
|
||||
JSONObject o = loadedSettings.getJSONObject(id);
|
||||
|
||||
// Legacy version support: On versions 0.3.3 and older, the repeat mode was represented as a boolean.
|
||||
if (!o.has("repeat_mode") && o.has("repeat") && o.getBoolean("repeat"))
|
||||
o.put("repeat_mode", RepeatMode.ALL);
|
||||
|
||||
|
||||
settings.put(Long.parseLong(id), new Settings(this,
|
||||
o.has("text_channel_id") ? o.getString("text_channel_id") : null,
|
||||
o.has("voice_channel_id")? o.getString("voice_channel_id") : null,
|
||||
o.has("dj_role_id") ? o.getString("dj_role_id") : null,
|
||||
o.has("volume") ? o.getInt("volume") : 100,
|
||||
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") : -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) {
|
||||
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
|
||||
*/
|
||||
@Override
|
||||
public Settings getSettings(Guild guild)
|
||||
{
|
||||
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, -1, QueueType.FAIR, 0L, StatsReportFrequency.OFF);
|
||||
}
|
||||
|
||||
protected void writeSettings()
|
||||
{
|
||||
JSONObject obj = new JSONObject();
|
||||
settings.keySet().stream().forEach(key -> {
|
||||
JSONObject o = new JSONObject();
|
||||
Settings s = settings.get(key);
|
||||
if(s.textId!=0)
|
||||
o.put("text_channel_id", Long.toString(s.textId));
|
||||
if(s.voiceId!=0)
|
||||
o.put("voice_channel_id", Long.toString(s.voiceId));
|
||||
if(s.roleId!=0)
|
||||
o.put("dj_role_id", Long.toString(s.roleId));
|
||||
if(s.getVolume()!=100)
|
||||
o.put("volume",s.getVolume());
|
||||
if(s.getDefaultPlaylist() != null)
|
||||
o.put("default_playlist", s.getDefaultPlaylist());
|
||||
if(s.getRepeatMode()!=RepeatMode.OFF)
|
||||
o.put("repeat_mode", s.getRepeatMode());
|
||||
if(s.getPrefix() != null)
|
||||
o.put("prefix", s.getPrefix());
|
||||
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(SETTINGS_FILE), obj.toString(4).getBytes());
|
||||
} catch(IOException 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2016 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;
|
||||
|
||||
import com.jagrosh.jmusicbot.audio.RequestMetadata.UserInfo;
|
||||
import java.util.List;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
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 formatUsername(String username, String discrim)
|
||||
{
|
||||
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 = "";
|
||||
for(int i=0; i<12; i++)
|
||||
if(i == (int)(percent*12))
|
||||
str+="\uD83D\uDD18"; // 🔘
|
||||
else
|
||||
str+="▬";
|
||||
return str;
|
||||
}
|
||||
|
||||
public static String volumeIcon(int volume)
|
||||
{
|
||||
if(volume == 0)
|
||||
return "\uD83D\uDD07"; // 🔇
|
||||
if(volume < 30)
|
||||
return "\uD83D\uDD08"; // 🔈
|
||||
if(volume < 70)
|
||||
return "\uD83D\uDD09"; // 🔉
|
||||
return "\uD83D\uDD0A"; // 🔊
|
||||
}
|
||||
|
||||
public static String listOfTChannels(List<TextChannel> list, String query)
|
||||
{
|
||||
String out = " Multiple text channels found matching \""+query+"\":";
|
||||
for(int i=0; i<6 && i<list.size(); i++)
|
||||
out+="\n - "+list.get(i).getName()+" (<#"+list.get(i).getId()+">)";
|
||||
if(list.size()>6)
|
||||
out+="\n**And "+(list.size()-6)+" more...**";
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String listOfVChannels(List<VoiceChannel> list, String query)
|
||||
{
|
||||
String out = " Multiple voice channels found matching \""+query+"\":";
|
||||
for(int i=0; i<6 && i<list.size(); i++)
|
||||
out+="\n - "+list.get(i).getAsMention()+" (ID:"+list.get(i).getId()+")";
|
||||
if(list.size()>6)
|
||||
out+="\n**And "+(list.size()-6)+" more...**";
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String listOfRoles(List<Role> list, String 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)
|
||||
out+="\n**And "+(list.size()-6)+" more...**";
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String filter(String input)
|
||||
{
|
||||
return input.replace("\u202E","")
|
||||
.replace("@everyone", "@\u0435veryone") // cyrillic letter e
|
||||
.replace("@here", "@h\u0435re") // cyrillic letter e
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright 2018 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;
|
||||
|
||||
import com.jagrosh.jdautilities.commons.JDAUtilitiesInfo;
|
||||
import com.jagrosh.jmusicbot.JMusicBot;
|
||||
import com.jagrosh.jmusicbot.entities.Prompt;
|
||||
import java.io.*;
|
||||
import java.net.URISyntaxException;
|
||||
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;
|
||||
import org.json.JSONTokener;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author John Grosh <john.a.grosh@gmail.com>
|
||||
*/
|
||||
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/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\\";
|
||||
|
||||
/**
|
||||
* gets a Path from a String
|
||||
* also fixes the windows tendency to try to start in system32
|
||||
* any time the bot tries to access this path, it will instead start in the location of the jar file
|
||||
*
|
||||
* @param path the string path
|
||||
* @return the Path object
|
||||
*/
|
||||
public static Path getPath(String path)
|
||||
{
|
||||
Path result = Paths.get(path);
|
||||
// special logic to prevent trying to access system32
|
||||
if(result.toAbsolutePath().toString().toLowerCase().startsWith(WINDOWS_INVALID_PATH))
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Paths.get(new File(JMusicBot.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getPath() + File.separator + path);
|
||||
}
|
||||
catch(URISyntaxException ignored) {}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a resource from the jar as a string
|
||||
*
|
||||
* @param clazz class base object
|
||||
* @param name name of resource
|
||||
* @return string containing the contents of the resource
|
||||
*/
|
||||
public static String loadResource(Object clazz, String name)
|
||||
{
|
||||
try(BufferedReader reader = new BufferedReader(new InputStreamReader(clazz.getClass().getResourceAsStream(name))))
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
reader.lines().forEach(line -> sb.append("\r\n").append(line));
|
||||
return sb.toString().trim();
|
||||
}
|
||||
catch(IOException ignored)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads image data from a URL
|
||||
*
|
||||
* @param url url of image
|
||||
* @return inputstream of url
|
||||
*/
|
||||
public static InputStream imageFromUrl(String url)
|
||||
{
|
||||
if(url==null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
URL u = new URL(url);
|
||||
URLConnection urlConnection = u.openConnection();
|
||||
urlConnection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36");
|
||||
return urlConnection.getInputStream();
|
||||
}
|
||||
catch(IOException | IllegalArgumentException ignore) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an activity from a string
|
||||
*
|
||||
* @param game the game, including the action such as 'playing' or 'watching'
|
||||
* @return the parsed activity
|
||||
*/
|
||||
public static Activity parseGame(String game)
|
||||
{
|
||||
if(game==null || game.trim().isEmpty() || game.trim().equalsIgnoreCase("default"))
|
||||
return null;
|
||||
String lower = game.toLowerCase();
|
||||
if(lower.startsWith("playing"))
|
||||
return Activity.playing(makeNonEmpty(game.substring(7).trim()));
|
||||
if(lower.startsWith("listening to"))
|
||||
return Activity.listening(makeNonEmpty(game.substring(12).trim()));
|
||||
if(lower.startsWith("listening"))
|
||||
return Activity.listening(makeNonEmpty(game.substring(9).trim()));
|
||||
if(lower.startsWith("watching"))
|
||||
return Activity.watching(makeNonEmpty(game.substring(8).trim()));
|
||||
if(lower.startsWith("streaming"))
|
||||
{
|
||||
String[] parts = game.substring(9).trim().split("\\s+", 2);
|
||||
if(parts.length == 2)
|
||||
{
|
||||
return Activity.streaming(makeNonEmpty(parts[1]), "https://twitch.tv/"+parts[0]);
|
||||
}
|
||||
}
|
||||
return Activity.playing(game);
|
||||
}
|
||||
|
||||
public static String makeNonEmpty(String str)
|
||||
{
|
||||
return str == null || str.isEmpty() ? "\u200B" : str;
|
||||
}
|
||||
|
||||
public static OnlineStatus parseStatus(String status)
|
||||
{
|
||||
if(status==null || status.trim().isEmpty())
|
||||
return OnlineStatus.ONLINE;
|
||||
OnlineStatus st = OnlineStatus.fromKey(status);
|
||||
return st == null ? OnlineStatus.ONLINE : st;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Check for new version
|
||||
String latestVersion = getLatestVersion();
|
||||
|
||||
if(latestVersion!=null && !latestVersion.equals(version))
|
||||
{
|
||||
prompt.alert(Prompt.Level.WARNING, "JMusicBot Version", String.format(NEW_VERSION_AVAILABLE, version, latestVersion));
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCurrentVersion()
|
||||
{
|
||||
if(JMusicBot.class.getPackage()!=null && JMusicBot.class.getPackage().getImplementationVersion()!=null)
|
||||
return JMusicBot.class.getPackage().getImplementationVersion();
|
||||
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/chrisb09/MusicBot/releases/latest").build())
|
||||
.execute();
|
||||
ResponseBody body = response.body();
|
||||
if(body != null)
|
||||
{
|
||||
try(Reader reader = body.charStream())
|
||||
{
|
||||
JSONObject obj = new JSONObject(new JSONTokener(reader));
|
||||
return obj.getString("tag_name");
|
||||
}
|
||||
finally
|
||||
{
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
else
|
||||
return null;
|
||||
}
|
||||
catch(IOException | JSONException | NullPointerException ex)
|
||||
{
|
||||
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: [+ | -] <HH:MM:SS | MM:SS | SS>
|
||||
* @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,418 @@
|
||||
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 com.sun.net.httpserver.HttpServer;
|
||||
import dev.lavalink.youtube.clients.Web;
|
||||
import dev.lavalink.youtube.clients.WebEmbedded;
|
||||
import org.json.JSONObject;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
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.logging.LogEntries;
|
||||
import org.openqa.selenium.logging.LogEntry;
|
||||
import org.openqa.selenium.logging.LogType;
|
||||
import org.openqa.selenium.logging.LoggingPreferences;
|
||||
import org.openqa.selenium.os.ExecutableFinder;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
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());
|
||||
|
||||
// Essential flags for headless/containerized environments (Docker)
|
||||
chromeOptions.addArguments("--no-sandbox");
|
||||
chromeOptions.addArguments("--disable-dev-shm-usage");
|
||||
chromeOptions.addArguments("--disable-gpu");
|
||||
chromeOptions.addArguments("--window-size=1920,1080");
|
||||
chromeOptions.addArguments("--disable-blink-features=AutomationControlled");
|
||||
chromeOptions.addArguments("--autoplay-policy=no-user-gesture-required");
|
||||
chromeOptions.addArguments("--disable-features=IsolateOrigins,site-per-process");
|
||||
chromeOptions.addArguments("--disable-site-isolation-trials");
|
||||
|
||||
// Enable Chrome performance logging to capture Network.requestWillBeSent events
|
||||
LoggingPreferences logPrefs = new LoggingPreferences();
|
||||
logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
|
||||
chromeOptions.setCapability("goog:loggingPrefs", logPrefs);
|
||||
|
||||
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. Start local HTTP server to host iframe embed & extract PO token from performance logs
|
||||
//
|
||||
HttpServer server = null;
|
||||
try {
|
||||
// Start a lightweight local HTTP server to host the YouTube embed in an iframe.
|
||||
// This provides a valid origin/Referer (http://127.0.0.1:PORT) so YouTube does not
|
||||
// reject the embed with Error 153 "Video player configuration error".
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/embed", exchange -> {
|
||||
String html = "<!DOCTYPE html>" +
|
||||
"<html>" +
|
||||
"<head>" +
|
||||
"<meta name=\"referrer\" content=\"strict-origin-when-cross-origin\">" +
|
||||
"<style>html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000;}" +
|
||||
"iframe{width:100%;height:100%;border:none;}</style>" +
|
||||
"</head>" +
|
||||
"<body>" +
|
||||
"<iframe id=\"player\" " +
|
||||
"src=\"https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ?autoplay=1&mute=1&enablejsapi=1\" " +
|
||||
"referrerpolicy=\"strict-origin-when-cross-origin\" " +
|
||||
"allow=\"autoplay; encrypted-media; picture-in-picture\" " +
|
||||
"allowfullscreen></iframe>" +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
|
||||
exchange.getResponseHeaders().set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
int serverPort = server.getAddress().getPort();
|
||||
|
||||
// First, navigate to youtube.com to inject consent cookies to bypass EU/GDPR consent walls
|
||||
try {
|
||||
driver.get("https://www.youtube.com");
|
||||
driver.manage().addCookie(new Cookie.Builder("SOCS", "CAESEwgDEgk0ODE3Nzk3MjQaAmVuIAEaBgiA_LyaBg")
|
||||
.domain(".youtube.com")
|
||||
.path("/")
|
||||
.build());
|
||||
driver.manage().addCookie(new Cookie.Builder("CONSENT", "PENDING+999")
|
||||
.domain(".youtube.com")
|
||||
.path("/")
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Could not pre-set consent cookies: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Navigate to local embed wrapper page
|
||||
driver.get("http://127.0.0.1:" + serverPort + "/embed");
|
||||
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
|
||||
// Wait for iframe and switch to it to interact with player
|
||||
try {
|
||||
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.id("player")));
|
||||
|
||||
// Dismiss any consent dialog inside the iframe if present
|
||||
try {
|
||||
List<WebElement> consentButtons = driver.findElements(By.xpath(
|
||||
"//button[contains(@aria-label, 'Agree') or contains(@aria-label, 'Accept') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'alle akzeptieren') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ich stimme zu') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'tout accepter') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accepter tout') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accetta tutto') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'aceptar todo')]"
|
||||
));
|
||||
for (WebElement btn : consentButtons) {
|
||||
if (btn.isDisplayed()) {
|
||||
LOGGER.info("Dismissing consent dialog inside iframe...");
|
||||
btn.click();
|
||||
Thread.sleep(1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// Try to click player inside iframe to trigger playback and /player request
|
||||
try {
|
||||
WebElement moviePlayer = wait.until(ExpectedConditions.presenceOfElementLocated(
|
||||
By.cssSelector("#movie_player, .ytp-large-play-button, .html5-video-player, video")
|
||||
));
|
||||
moviePlayer.click();
|
||||
} catch (Exception ignored) {
|
||||
((JavascriptExecutor) driver).executeScript(
|
||||
"var p = document.getElementById('movie_player') || document.querySelector('video'); " +
|
||||
"if (p) { if (p.playVideo) p.playVideo(); else if (p.play) p.play(); }"
|
||||
);
|
||||
}
|
||||
|
||||
// Switch back to default context so performance logging and main window operations work
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Could not interact with embed iframe: {}", e.getMessage());
|
||||
try {
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Poll performance logs to capture the /youtubei/v1/player request
|
||||
boolean success = false;
|
||||
long deadline = System.currentTimeMillis() + 25_000;
|
||||
long nextActionTime = System.currentTimeMillis() + 5_000;
|
||||
|
||||
while (System.currentTimeMillis() < deadline && !success) {
|
||||
LogEntries logEntries = driver.manage().logs().get(LogType.PERFORMANCE);
|
||||
for (LogEntry entry : logEntries) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(entry.getMessage());
|
||||
JSONObject message = json.optJSONObject("message");
|
||||
if (message == null) continue;
|
||||
|
||||
String method = message.optString("method");
|
||||
if ("Network.requestWillBeSent".equals(method)) {
|
||||
JSONObject params = message.optJSONObject("params");
|
||||
if (params == null) continue;
|
||||
JSONObject req = params.optJSONObject("request");
|
||||
if (req == null) continue;
|
||||
String url = req.optString("url", "");
|
||||
if (url.contains("/youtubei/v1/player")) {
|
||||
String postData = decodePostData(req);
|
||||
|
||||
if (postData == null || postData.isEmpty()) {
|
||||
LOGGER.warn("Saw YouTube player request without a readable body, waiting for another...");
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject body;
|
||||
try {
|
||||
body = new JSONObject(postData);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("YouTube player request body is not JSON ({}), waiting for another...", e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
String visitorData = null;
|
||||
if (body.has("context") && body.getJSONObject("context").has("client")) {
|
||||
visitorData = body.getJSONObject("context").getJSONObject("client").optString("visitorData", null);
|
||||
}
|
||||
|
||||
String poToken = null;
|
||||
if (body.has("serviceIntegrityDimensions")) {
|
||||
poToken = body.getJSONObject("serviceIntegrityDimensions").optString("poToken", null);
|
||||
}
|
||||
|
||||
if (visitorData != null && poToken != null) {
|
||||
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
|
||||
LOGGER.debug("PO token: {}", poToken);
|
||||
LOGGER.debug("Visitor Data: {}", visitorData);
|
||||
|
||||
// Set PO token and visitor data on both Web and WebEmbedded clients
|
||||
Web.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
WebEmbedded.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Error parsing performance log entry: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (success) break;
|
||||
|
||||
// If still not captured after a while, try re-triggering play inside iframe
|
||||
if (System.currentTimeMillis() > nextActionTime) {
|
||||
try {
|
||||
driver.switchTo().frame("player");
|
||||
((JavascriptExecutor) driver).executeScript(
|
||||
"var p = document.getElementById('movie_player') || document.querySelector('video'); " +
|
||||
"if (p) { if (p.playVideo) p.playVideo(); else if (p.play) p.play(); }"
|
||||
);
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
nextActionTime = System.currentTimeMillis() + 5_000;
|
||||
}
|
||||
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
saveDebugPage(driver);
|
||||
throw new Exception("Timed out waiting for YouTube player request. Current URL: " + driver.getCurrentUrl());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to obtain PO token for YouTube playback: {}", e.getMessage());
|
||||
LOGGER.debug("Exception:", e);
|
||||
} finally {
|
||||
if (server != null) {
|
||||
try {
|
||||
server.stop(0);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
// Only quit the browser when in headless mode. Helps with troubleshooting.
|
||||
if (headless)
|
||||
driver.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the request body of a captured Network.requestWillBeSent event.
|
||||
* Prefers the raw Base64 postDataEntries (Chrome 130+) over the lossy postData string,
|
||||
* and transparently gunzips the body when the client sent it with Content-Encoding: gzip
|
||||
* (YouTube web client 2.20260918+ compresses the /youtubei/v1/player request).
|
||||
*/
|
||||
private static String decodePostData(JSONObject req) {
|
||||
byte[] raw = null;
|
||||
org.json.JSONArray entries = req.optJSONArray("postDataEntries");
|
||||
if (entries != null) {
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < entries.length(); i++) {
|
||||
JSONObject entryObj = entries.optJSONObject(i);
|
||||
if (entryObj != null && entryObj.has("bytes")) {
|
||||
try {
|
||||
buf.write(java.util.Base64.getDecoder().decode(entryObj.getString("bytes")));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
if (buf.size() > 0) raw = buf.toByteArray();
|
||||
}
|
||||
if (raw == null) {
|
||||
String postData = req.optString("postData", null);
|
||||
if (postData == null || postData.isEmpty()) return null;
|
||||
raw = postData.getBytes(StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
if (raw.length > 2 && (raw[0] & 0xff) == 0x1f && (raw[1] & 0xff) == 0x8b) {
|
||||
try (GZIPInputStream gz = new GZIPInputStream(new ByteArrayInputStream(raw))) {
|
||||
raw = gz.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to gunzip YouTube player request body: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return new String(raw, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void saveDebugPage(WebDriver driver) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String currentUrl = driver.getCurrentUrl();
|
||||
sb.append("<!-- Current URL: ").append(currentUrl).append(" -->\n");
|
||||
sb.append("<!-- MAIN PAGE SOURCE -->\n");
|
||||
try {
|
||||
sb.append(driver.getPageSource()).append("\n\n");
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
try {
|
||||
driver.switchTo().frame("player");
|
||||
sb.append("<!-- IFRAME SOURCE -->\n");
|
||||
sb.append(driver.getPageSource()).append("\n");
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
File debugFile = new File("youtube_debug.html");
|
||||
try (FileWriter writer = new FileWriter(debugFile)) {
|
||||
writer.write(sb.toString());
|
||||
}
|
||||
LOGGER.info("Saved YouTube debug page source to {} (Current URL: {})", debugFile.getAbsolutePath(), currentUrl);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to save YouTube debug page source: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user