Compare commits

..

No commits in common. "main" and "dev" have entirely different histories.
main ... dev

13 changed files with 169 additions and 269 deletions

View File

@ -25,7 +25,7 @@
# Overview
Guava is a Discord music bot with support for multiple different music and video streaming platforms. Guava is a part of >225 Discord servers and currently supports these services:
Guava is a Discord music bot with support for multiple different music and video streaming platforms. Guava is a part of >200 Discord servers and currently supports these services:
- YouTube
- Apple Music
@ -106,11 +106,10 @@ Field | Description | Requirement
GENIUS_CLIENT_ID | `CLIENT ID`: ID from Genius API Dashboard | **OPTIONAL** - *Used for the /lyrics command*
GENIUS_CLIENT_SECRET | `CLIENT SECRET`: Secret string from Genius API Dashboard | **OPTIONAL** - *Used for the /lyrics command*
## AI | OPTIONAL
## OPENAI | OPTIONAL
Field | Description | Requirement
--- | --- | ---
SERVICE | Which providers API you will use. Supports `groq` or `openai` | **OPTIONAL** - *Used to support the /autoplay feature*
API_KEY | API key for the provider you are using | **OPTIONAL** - *Used to support the /autoplay feature*
OPENAI_API_KEY | API Key from OpenAI for autoplay recommendations | **OPTIONAL** - *Used to support the /autoplay feature*
<br>

View File

@ -1,123 +1,120 @@
server:
port: 2333
address: 127.0.0.1
http2:
enabled: false
port: 2333
address: localhost
http2:
enabled: false
plugins:
youtube:
enabled: true
allowSearch: true
allowDirectVideoIds: true
allowDirectPlaylistIds: true
clients:
- MUSIC
- ANDROID_VR
- WEB
- WEBEMBEDDED
pot: # https://github.com/iv-org/youtube-trusted-session-generator
token: ""
visitorData: ""
lavasrc:
providers:
- "scsearch:\"%ISRC%\""
- "scsearch:%QUERY%"
- "dzisrc:\"%ISRC%\""
- "dzsearch:%QUERY%"
sources:
spotify: false
applemusic: false
deezer: true
yandexmusic: false
flowerytts: false
youtube: false
deezer:
masterDecryptionKey: "" # Find on your own
arl: "" # Guides can be found, use Google
formats: [ "MP3_128", "MP3_64" ] # "FLAC", "MP3_320", "MP3_256", & "AAC_64" require premium and valid arl
youtube:
enabled: true
allowSearch: true
allowDirectVideoIds: true
allowDirectPlaylistIds: true
clients:
- MUSIC
- ANDROID_VR
- WEB
- WEBEMBEDDED
pot:
token: "" # Your token data here
visitorData: "" # Your visitor data here
lavasrc:
providers:
- "scsearch:\"%ISRC%\""
- "scsearch:%QUERY%"
- "dzisrc:\"%ISRC%\""
- "dzsearch:%QUERY%"
sources:
spotify: false
applemusic: false
deezer: true
yandexmusic: false
flowerytts: falsee
youtube: false
deezer:
masterDecryptionKey: "" # master decryption key from deezer
lavalink:
plugins:
- dependency: "dev.lavalink.youtube:youtube-plugin:5ce67f60c656e0dc60687ea7b471663c8718ea1c"
repository: "https://maven.kikkia.dev/snapshots"
snapshot: true
- dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.4.2"
snapshot: false
plugins:
- dependency: "dev.lavalink.youtube:youtube-plugin:1.11.1"
snapshot: false
- dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.3.0"
snapshot: false
server:
password: "youshallnotpass"
sources:
youtube: false
bandcamp: true
soundcloud: true
twitch: true
vimeo: true
http: true
local: false
filters:
volume: false
equalizer: false
karaoke: false
timescale: false
tremolo: false
vibrato: false
distortion: false
rotation: false
channelMix: false
lowPass: false
server:
password: "youshallnotpass"
sources:
youtube: false
bandcamp: true
soundcloud: true
twitch: true
vimeo: true
http: true
local: false
filters:
volume: false
equalizer: false
karaoke: false
timescale: false
tremolo: false
vibrato: false
distortion: false
rotation: false
channelMix: false
lowPass: false
bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses.
frameBufferDurationMs: 8000 # How many milliseconds of audio to keep buffered
opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
youtubePlaylistLoadLimit: 3 # Number of pages at 100 each
playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds
youtubeSearchEnabled: true
soundcloudSearchEnabled: true
gc-warnings: true
# ratelimit:
# ipBlocks: [""] # list of ip blocks
# excludedIps: [] # ips which should be explicit excluded from usage by lavalink
# strategy: "LoadBalance" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
# searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
# retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
# httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
# proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
# proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
# proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
# proxyPassword: "" # Password for basic authentication
bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses.
frameBufferDurationMs: 8000 # How many milliseconds of audio to keep buffered
opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds
youtubeSearchEnabled: true
soundcloudSearchEnabled: true
gc-warnings: true
#ratelimit:
# ipBlocks: [""] # list of ip blocks
# excludedIps: [] # ips which should be explicit excluded from usage by lavalink
# strategy: "LoadBalance" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
# searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
# retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
#httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
#proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
#proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
#proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
#proxyPassword: "" # Password for basic authentication
metrics:
prometheus:
enabled: false
endpoint: /metrics
prometheus:
enabled: false
endpoint: /metrics
sentry:
dsn: ""
environment: ""
# tags:
# some_key: some_value
# another_key: another_value
dsn: ""
environment: ""
# tags:
# some_key: some_value
# another_key: another_value
logging:
file:
path: ./logs/
file:
path: ./logs/
level:
root: INFO
lavalink: INFO
level:
root: INFO
lavalink: INFO
request:
enabled: true
includeClientInfo: true
includeHeaders: false
includeQueryString: true
includePayload: true
maxPayloadLength: 10000
request:
enabled: true
includeClientInfo: true
includeHeaders: false
includeQueryString: true
includePayload: true
maxPayloadLength: 10000
logback:
rollingpolicy:
max-file-size: 1GB
max-history: 30
logback:
rollingpolicy:
max-file-size: 1GB
max-history: 30

View File

@ -2,6 +2,7 @@ import discord
from discord.ext import commands, tasks
import os
import requests
import openai
import lyricsgenius
import utils.config as config
@ -19,17 +20,19 @@ class MyBot(commands.Bot):
)
async def setup_hook(self):
# Get Spotify, Apple Music, and Genius access tokens/clients
# Get Spotify, Apple Music, Genius, and OpenAI access tokens/clients
get_access_token.start()
refresh_media_api_key.start()
login_genius.start()
if config.OPENAI_API_KEY:
bot.openai = openai.OpenAI(api_key=config.OPENAI_API_KEY)
config.LOG.info("Loading cogs...")
if config.YOUTUBE_SUPPORT:
config.LOG.info(
"YouTube support is enabled, make sure to set a poToken"
)
else:
config.LOG.warn("YouTube support is disabled")
config.LOG.info(
"YouTube support is enabled, make sure to set a poToken"
if config.YOUTUBE_SUPPORT
else "YouTube support is disabled"
)
for ext in os.listdir("./code/cogs"):
if ext.endswith(".py"):
# Load the OPTIONAL feedback cog
@ -37,28 +40,28 @@ class MyBot(commands.Bot):
ext[:-3] == "feedback"
and config.FEEDBACK_CHANNEL_ID == None
):
config.LOG.warn(
config.LOG.info(
"Skipped loading feedback cog - channel ID not"
" provided"
)
continue
# Load the OPTIONAL bug cog
if ext[:-3] == "bug" and config.BUG_CHANNEL_ID == None:
config.LOG.warn(
config.LOG.info(
"Skipped loading bug cog - channel ID not provided"
)
continue
# Load the OPTIONAL lyrics cog
if ext[:-3] == "lyrics" and config.GENIUS_CLIENT_ID == None:
config.LOG.warn(
config.LOG.info(
"Skipped loading lyrics cog - Genius API credentials"
" not provided"
)
continue
# Load the OPTIONAL autoplay cog
if ext[:-3] == "autoplay" and config.AI_CLIENT == None:
config.LOG.warn(
"Skipped loading autoplay cog - AI API credentials"
if ext[:-3] == "autoplay" and config.OPENAI_API_KEY == None:
config.LOG.info(
"Skipped loading autoplay cog - OpenAI API credentials"
" not provided"
)
continue
@ -80,7 +83,6 @@ bot = MyBot()
bot.remove_command("help")
bot.temp_command_count = {} # command_name: count
bot.autoplay = [] # guild_id, guild_id, etc.
bot.youtube_broken = False
@tasks.loop(minutes=45)

View File

@ -75,7 +75,9 @@ class Autoplay(commands.Cog):
)
await interaction.response.send_message(embed=embed)
if await add_song_recommendations(self.bot.user, player, 5, inputs):
if await add_song_recommendations(
self.bot.openai, self.bot.user, player, 5, inputs
):
self.bot.autoplay.append(interaction.guild.id)
embed = create_embed(
title=":infinity: Autoplay Enabled :infinity:",

View File

@ -56,8 +56,7 @@ commands_and_descriptions = {
},
"autoplay": {
"description": (
"Keep the music playing forever with automatic song"
" recommendations"
"Keep the music playing forever with music suggestions from OpenAI"
),
"arguments": {
"on": "Turn autoplay feature on",

View File

@ -245,7 +245,7 @@ class Music(commands.Cog):
for song in event.player.queue[:10]:
inputs[song.title] = song.author
await add_song_recommendations(
self.bot.user, event.player, 5, inputs
self.bot.openai, self.bot.user, event.player, 5, inputs
)
@lavalink.listener(lavalink.events.NodeConnectedEvent)

View File

@ -1,29 +0,0 @@
from discord.ext import commands
class Send(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.dm_only()
@commands.is_owner()
async def send(self, ctx, user_id: int, *, message: str):
"""Send a message to a user (follow-up on bug reports)"""
user = await self.bot.fetch_user(user_id)
if not user:
return await ctx.send("User not found.")
elif not message:
return await ctx.send("No message for user.")
try:
await user.send(message)
await ctx.send("Message sent to user.")
except Exception as e:
await ctx.send("Error sending message to user.")
async def setup(bot):
await bot.add_cog(Send(bot))

View File

@ -1,31 +0,0 @@
from discord.ext import commands
from typing import Literal
class Toggle(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.dm_only()
@commands.is_owner()
async def toggle(self, ctx, action: Literal["on", "off"]):
"""Toggle YouTube as broken or not"""
if action == "on":
self.bot.youtube_broken = False
return await ctx.send("YouTube has been enabled.")
if action == "off":
self.bot.youtube_broken = True
return await ctx.send("YouTube has been marked as broken.")
@toggle.error
async def toggle_error(self, ctx, error):
if isinstance(error, commands.BadLiteralArgument):
return await ctx.send("Invalid action. Use either 'on' or 'off'.")
else:
return await ctx.send("An unknown error occurred.")
async def setup(bot):
await bot.add_cog(Toggle(bot))

View File

@ -45,22 +45,6 @@ class Play(commands.Cog):
embed=embed, ephemeral=True
)
if self.bot.youtube_broken:
embed = create_embed(
title="YouTube Broken",
description=(
"YouTube support is currently broken. This is a known"
" issue and is being actively worked on, please try"
" again later. Other sources should still be in"
" working order. Submit a bug report with "
" </bug:1224840889906499626> if issues persist. Sorry"
" for the inconvenience."
),
)
return await interaction.response.send_message(
embed=embed, ephemeral=True
)
# Check for custom sources (Apple Music/Spotify)
if "music.apple.com" in query:
results, embed = await parse_custom_source(

View File

@ -1,52 +1,40 @@
from lavalink import LoadType
import re
from utils.config import AI_CLIENT, AI_MODEL
async def add_song_recommendations(
bot_user, player, number, inputs, retries: int = 1
openai_client, bot_user, player, number, inputs, retries: int = 1
):
input_list = [f'"{song} by {artist}"' for song, artist in inputs.items()]
completion = (
AI_CLIENT.chat.completions.create(
openai_client.chat.completions.create(
messages=[
{
"role": "system",
"content": f"""
Given an input list of songs formatted as ["song_name
by artist_name", "song_name by artist_name", ...], generate
a list of 5 new songs that the user may enjoy based on
the input.
Thoroughly analyze each song in the input list, considering
factors such as tempo, beat, mood, genre, lyrical themes,
instrumentation, and overall meaning. Use this analysis to
recommend 5 songs that closely align with the user's musical
preferences.
The output must be formatted in the exact same way:
["song_name by artist_name", "song_name by artist_name", ...].
If you are unable to find 5 new songs or encounter any issues,
return the following list instead: ["NOTHING_FOUND"]. Do
not return partial resultseither provide 5 songs or return
["NOTHING_FOUND"]. Ensure accuracy in song and artist names.
DO NOT include any additional information or text in the
output, it should STRICTLY be either a list of the songs
or ["NOTHING_FOUND"].
""",
},
{
"role": "user",
"content": f"""
{input_list}
BACKGROUND: You're an AI music recommendation system with a knack for understanding
user preferences based on provided input. Your task is to generate a list
of {number} songs that the user might enjoy, derived from a given list of {number} songs.
The input will be in the format of
["Song-1-Name by Song-1-Artist", "Song-2-Name by Song-2-Artist", ...]
and you need to return a list formatted in the same way.
When recommending songs, consider the genre, tempo, and mood of the input
songs to suggest similar ones that align with the user's tastes. Also, it
is important to mix up the artists, don't only give the same artists that
are already in the queue. If you cannot find {number} songs that match the
criteria or encounter any issues, return the list ["NOTHING FOUND"].
Please be sure to also only use characters A-Z, a-z, 0-9, and spaces in the
song and artist names. Do not include escape/special characters, emojis, or
quotes in the output.
INPUT: {input_list}
""",
},
}
],
model=AI_MODEL,
model="gpt-4o-mini",
)
.choices[0]
.message.content.strip()
@ -59,7 +47,7 @@ async def add_song_recommendations(
if completion == '["NOTHING FOUND"]':
if retries <= 3:
await add_song_recommendations(
bot_user, player, number, inputs, retries + 1
openai_client, bot_user, player, number, inputs, retries + 1
)
else:
return False

View File

@ -8,7 +8,7 @@ import sys
import discord
import logging
import requests
from groq import Groq
from datetime import datetime
from colorlog import ColoredFormatter
log_level = logging.DEBUG
@ -39,8 +39,7 @@ SPOTIFY_CLIENT_ID = None
SPOTIFY_CLIENT_SECRET = None
GENIUS_CLIENT_ID = None
GENIUS_CLIENT_SECRET = None
AI_CLIENT = None
AI_MODEL = None
OPENAI_API_KEY = None
LAVALINK_HOST = None
LAVALINK_PORT = None
LAVALINK_PASSWORD = None
@ -83,13 +82,12 @@ schema = {
},
"required": ["genius_client_id", "genius_client_secret"],
},
"ai": {
"openai": {
"type": "object",
"properties": {
"service": {"enum": ["openai", "groq"]},
"api_key": {"type": "string"},
"openai_api_key": {"type": "string"},
},
"required": ["service"],
"required": ["openai_api_key"],
},
"lavalink": {
"type": "object",
@ -146,9 +144,9 @@ genius:
genius_client_id:
genius_client_secret:
ai:
service:
api_key: """
openai:
openai_api_key:
"""
)
sys.exit(
@ -162,7 +160,7 @@ ai:
# Thouroughly validate all of the options in the config.yaml file
def validate_config(file_contents):
global TOKEN, BOT_COLOR, BOT_INVITE_LINK, FEEDBACK_CHANNEL_ID, BUG_CHANNEL_ID, LOG_SONGS, YOUTUBE_SUPPORT, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, GENIUS_CLIENT_ID, GENIUS_CLIENT_SECRET, AI_CLIENT, AI_MODEL, LAVALINK_HOST, LAVALINK_PORT, LAVALINK_PASSWORD
global TOKEN, BOT_COLOR, BOT_INVITE_LINK, FEEDBACK_CHANNEL_ID, BUG_CHANNEL_ID, LOG_SONGS, YOUTUBE_SUPPORT, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, GENIUS_CLIENT_ID, GENIUS_CLIENT_SECRET, OPENAI_API_KEY, LAVALINK_HOST, LAVALINK_PORT, LAVALINK_PASSWORD
config = yaml.safe_load(file_contents)
try:
@ -272,24 +270,17 @@ def validate_config(file_contents):
)
#
# If the AI section is present, make sure the API key is valid
# If the OPENAI section is present, make sure the API key is valid
#
if "ai" in config:
if config["ai"]["service"] == "openai":
client = openai.OpenAI(api_key=config["ai"]["api_key"])
model = "gpt-4o-mini"
elif config["ai"]["service"] == "groq":
client = Groq(api_key=config["ai"]["api_key"])
model = "llama-3.3-70b-specdec"
if "openai" in config:
client = openai.OpenAI(api_key=config["openai"]["openai_api_key"])
try:
client.models.list()
AI_CLIENT = client
AI_MODEL = model
OPENAI_API_KEY = config["openai"]["openai_api_key"]
except openai.AuthenticationError:
LOG.critical(
"Error in config.yaml file: OpenAI/Groq API key is invalid"
"Error in config.yaml file: OpenAI API key is invalid"
)
# Set appropriate values for all non-optional variables

View File

@ -22,6 +22,5 @@ genius:
genius_client_id:
genius_client_secret:
ai:
service:
api_key:
openai:
openai_api_key:

View File

@ -4,7 +4,6 @@ colorlog==6.8.2
validators==0.28.3
openai==1.56.0
requests==2.32.3
lyricsgenius==3.2.0
lyricsgenius==3.0.1
PyYAML==6.0.1
jsonschema==4.23.0
groq==0.18.0
jsonschema==4.23.0