diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c20fbac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +# .dockerignore + +# Git +.git +.gitignore +.gitattributes + +# Docker +docker-compose.yaml +Dockerfile +.docker +.dockerignore + +# Cached Files # +*.pyc +**/__pycache__ + +# Documentation +README.md + +#User Data +userconfig.yaml +exampleconfig.yaml +/user_config + +# Backup +**/config.py.bak \ No newline at end of file diff --git a/.gitignore b/.gitignore index 78d5d28..9c1354f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,15 @@ Thumbs.db ################# private.py config.py +config.py.bak +userconfig.yaml +exampleconfig.yaml +/user_config -# Directories # -############### -ffmpeg-2021-11-22/ +# Docker +docker-compose.yaml +#Dockerfile +.docker # Cached Files # ################ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..58da041 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12.5-alpine + +WORKDIR /rainwavediscordbot + +COPY ./requirements.txt /rainwavediscordbot +RUN pip install -r requirements.txt +RUN apk add ffmpeg +RUN apk add opus + +ENV DISCORD_TOKEN= +ENV RAINWAVE_ID= +ENV RAINWAVE_KEY= +ENV LOG_LEVEL="INFO" + +COPY . /rainwavediscordbot + +CMD [ "python", "./app/rainwavebot.py" ] + +#Build command `docker build -t rainwavetest .` \ No newline at end of file diff --git a/data/logo.png b/app/data/logo.png similarity index 100% rename from data/logo.png rename to app/data/logo.png diff --git a/app/load_config/defaultconfig.yaml b/app/load_config/defaultconfig.yaml new file mode 100644 index 0000000..e7c9c47 --- /dev/null +++ b/app/load_config/defaultconfig.yaml @@ -0,0 +1,72 @@ +botChannels: + #To restrict which voice channels the bot can use, set below to true. + restrictVoiceChannels: False + #If above is set to true, list allowed channel/s IDs in a list as shown below. + allowedVoiceChannels: [123456789012345678, 987654321098765432] + + #To restrict which test channels the bot can receive commands on, set below to true. + restrictTextChannels: False + #If above is set to true, list allowed channel/s IDs in a list as shown below. + allowedTextChannels: [112233445566778899, 998877665544332211] + + #If you want the bot to log, login and error info in a channel, set below to True + enableLogChannel: False + #If above is set to true, list your logging channel ID below + logChannel: 246813579024681357 + +private: + #Discord bot token + discordBotToken: F4K3T0K3N_ikb331nmGsvgHPGAv8jwFV3gKFs9eR.nF4lgje68ZdrEX9aSJ + + #Rainwave API ID + rainwaveID: 12345 + + #Rainwave API Key + rainwaveKey: 12345abcde + +dependencies: + #Location of FFMPEG file. + ffmpegLocation: "/usr/bin/ffmpeg" + + #Location of opus file. + opusLocation: "/usr/lib/libopus.so.0" + +options: + #Change the name of your bot here + botName: "Rain.Wave" + + #Change the prefix of your bot here + botPrefix: "rw." + + #Enables display of song progress in a numerical style timer e.g. [01:05/01:21] + enableProgressTimes: True + + #Enables display of song progress in a graphical manner e.g. ▰▰▰▱▱▱▱▱▱▱▱ + enableProgressBar: True + + #Allows selection of progress bar style between 1 and 2 + #1 is a left to right "fill" style progress bar e.g. ▰▰▰▱▱▱▱▱▱▱▱ + #2 is a moving indicator style progress bar e.g. ▱▱▱▰▱▱▱▱▱▱▱ + progressBarStyle: 1 + + #Allows selection of progress bar character length + progressBarLength: 14 + + #Allows selection of characters which make up the progress bar + #for style 1 ['▰','▱'] or ['▶','▷'] or ['█','░'] is suggested + #for style 2 ['═','╪'] or ['—','⎔'] or ['▬',':radio_button:'] is suggested + progressBarCharacters: ['▰','▱'] + + #Allows selection of embed sidebar color as an (r, g, b) value. + #You can use https://it-tools.tech/color-converter to generate an rgb color value + embedColor: [24, 135, 210] + + #Numer of seconds between refresh of progress bar and timer, can be increased if user is being rate limited. + #Minimum value can never be below 6. + refreshDelay: 6 + + #If set to `True`, bot will disconnect if no users are present in the bots voice channel. + autoDisconnect: True + + #Logging level, can be set to DEBUG, INFO, WARNING, ERROR, CRITICAL. If INFO provides too much info, switch to WARNING + logLevel: "INFO" \ No newline at end of file diff --git a/app/load_config/load_config.py b/app/load_config/load_config.py new file mode 100644 index 0000000..fadc736 --- /dev/null +++ b/app/load_config/load_config.py @@ -0,0 +1,63 @@ +import os +import shutil +import yaml #Needs to be added to docker setup. +from logger import logger + +OS_SLASH = '/' +USER_CONFIG_FOLDER = f"{OS_SLASH}user_config" +DEFAULT_CONFIG_PATH = f"{OS_SLASH}load_config{OS_SLASH}defaultconfig.yaml" +EXAMPLE_CONFIG_PATH = f"{USER_CONFIG_FOLDER}{OS_SLASH}exampleconfig.yaml" +USER_CONFIG_PATH = f"{USER_CONFIG_FOLDER}{OS_SLASH}userconfig.yaml" + +EXAMPLE_CONFIG_MESSAGE = "#This is the example config file. It is recreated on every launch.\n#ALL DATA STORED IN THIS FILE WILL BE DELETED!!!" +USER_CONFIG_MESSAGE = "#This is the user config file, changes to this file are persistent.\n#If you delete this file, it will be recreated and all default values will be restored." + +class config: + def __init__(self, startingPath): + with open(startingPath+DEFAULT_CONFIG_PATH, "r+") as openDefaultFile: + #NOTE this section fails on windows: `UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 in position 1992: character maps to ` + #which can be fixed with `encoding='utf-8'` or by adding a windows environmental variable `PYTHONUTF8` with a value of `1` + if os.path.isdir(startingPath+USER_CONFIG_FOLDER) is False: #Create /user_config if it doesn't exist + logger.debug(f"Creating {USER_CONFIG_FOLDER}") + os.mkdir(startingPath+USER_CONFIG_FOLDER) + else: + logger.debug(f"Located {USER_CONFIG_FOLDER}") + logger.debug(f"Config file info: {openDefaultFile}") + if os.path.isfile(startingPath+EXAMPLE_CONFIG_PATH): + logger.debug("Removing old exampleconfig.yaml") + os.remove(startingPath+EXAMPLE_CONFIG_PATH) + logger.debug("Creating exampleconfig.yaml") + with open(startingPath+EXAMPLE_CONFIG_PATH, 'a') as openExampleFile: + openExampleFile.write(f"{EXAMPLE_CONFIG_MESSAGE}\n\n") #write this comment into the first line + shutil.copyfileobj(openDefaultFile, openExampleFile) #append the default file to the example file + logger.debug("Created exampleconfig.yaml") + + openDefaultFile.seek(0) #This puts us back at the beginning of openDefaultFile, since we've already indexed to the end + + if os.path.isfile(startingPath+USER_CONFIG_PATH): + logger.info("Using previous userconfig.yaml") + else: + logger.debug("Creating userconfig.yaml") + with open(startingPath+USER_CONFIG_PATH, 'a') as openUserFile: + openUserFile.write(f"{USER_CONFIG_MESSAGE}\n\n") #write this comment into the first line + shutil.copyfileobj(openDefaultFile, openUserFile) #append the default file to the user file + logger.info("Created userconfig.yaml") + + logger.info("Loading config files") + with open(startingPath+DEFAULT_CONFIG_PATH, "r") as openDefaultFile: + defaultconfig = yaml.safe_load(openDefaultFile) + with open(startingPath+USER_CONFIG_PATH, "r") as openUserFile: + userconfig = yaml.safe_load(openUserFile) + self.config = {} + for section, item in defaultconfig.items(): + logger.debug(f"Loading {section} ------") + for key, value in item.items(): + try: + if key in userconfig[section]: + logger.debug(f"'{key}' found in userconfig") + self.config[key] = userconfig[section][key] + else: + logger.warning(f"'{key}' not found in user config, default loaded.") #Warn + self.config[key] = value + except: + logger.warning(f"Config section {section} not found") #Warn \ No newline at end of file diff --git a/app/logger.py b/app/logger.py new file mode 100644 index 0000000..9d70ed8 --- /dev/null +++ b/app/logger.py @@ -0,0 +1,17 @@ +import logging +import os +from sys import stdout + +#Possible log levels are, DEBUG, INFO, WARNING, ERROR, CRITICAL +#Written as debug(), info(), warning(), error() and critical() +logLevel = os.getenv("LOG_LEVEL", default="DEBUG") #Get loglevel environmental from docker + +#Set Up logger +logger = logging.getLogger('RWDB_Logger') #Create logger instance with an arbitrary name +logger.setLevel(logLevel) # set logger level +logFormatter = logging.Formatter\ +("%(asctime)s %(levelname)-8s %(filename)s:%(funcName)s:%(lineno)d %(message)s", "%Y-%m-%d %H:%M:%S") #What the log string looks like +consoleHandler = logging.StreamHandler(stdout) #set streamhandler to stdout +consoleHandler.setFormatter(logFormatter) #Apply the formatter +logger.addHandler(consoleHandler) #Apply stdout handler to logger +logger.info(f"Logger set to level: {logLevel}") \ No newline at end of file diff --git a/rainwavebot.py b/app/rainwavebot.py similarity index 69% rename from rainwavebot.py rename to app/rainwavebot.py index f91294f..28b29e3 100644 --- a/rainwavebot.py +++ b/app/rainwavebot.py @@ -4,7 +4,7 @@ import random #Built in import os #Built in import traceback #Built in -#import logging #Built in +import logging #Built in from datetime import datetime, timedelta, timezone #Built in #Libraries to install @@ -13,28 +13,30 @@ from discord.ext import commands from discord.ext import tasks import nacl #This import is not required, but provides `requirements.txt` clarity, and use of `pipreqs`. -from rainwaveclient import RainwaveClient #Command to upgrade the rainwaveclient api: pip install -U python-rainwave-client +from rainwaveclient import RainwaveClient #NOTE Command to upgrade the rainwaveclient api: pip install -U python-rainwave-client #Local imports -from config.config import botChannels -from config.config import private -from config.config import dependencies -from config.config import options +import load_config.load_config +#Global Constants MINIMUM_REFRESH_DELAY = 6 -#logging.basicConfig(level=logging.DEBUG) +#Load Config +config = load_config.load_config.config(os.path.dirname(os.path.abspath(__file__))) +config = config.config #Move the dict out of a class, for a shorter variable. +#Format for fetching config settings is `config["botPrefix"]` +#Set Up logger +from logger import logger + +#Discord Permissions intents = discord.Intents.default() intents.message_content = True intents.members = True -rainwaveClient = RainwaveClient() -rainwaveClient.user_id = private.rainwaveID -rainwaveClient.key = private.rainwaveKey - -bot = commands.Bot(command_prefix=options.botPrefix, - description=f"rainwave.cc bot, in development by Roach\nUse `{options.botPrefix}play` to get started", intents=intents) +#Create Bot instance w/ settings +bot = commands.Bot(command_prefix=config["botPrefix"], + description=f"rainwave.cc bot, in development by Roach\nUse `{config["botPrefix"]}play` to get started", intents=intents) class current: voiceChannel = None @@ -42,6 +44,11 @@ class current: playing = None message = None +class login: + discordBotToken = os.getenv("DISCORD_TOKEN", default=config["discordBotToken"]) + rainwaveID = os.getenv("RAINWAVE_ID", default=config["rainwaveID"]) + rainwaveKey = os.getenv("RAINWAVE_KEY", default=config["rainwaveKey"]) + def fetchMetaData(): return current.selectedStream.schedule_current.songs[0] @@ -55,12 +62,12 @@ def checkSyncThreadIsAlive(): try: if current.selectedStream._sync_thread.is_alive() == False: current.selectedStream.start_sync() - print("RW Sync Restarted") #TODO Logging + logger.debug("RW Sync Restarted") except Exception as returnedException: #print(f"checkSyncThreadIsAlive error: {returnedException}") #NOTE Not required here, but I want to keep it noted as an example. #traceback.print_exc() #NOTE Not required here, but I want to keep it noted as an example. current.selectedStream.start_sync() - print("RW Sync Started") #TODO Logging + logger.debug("RW Sync Started") async def postCurrentlyListening(ctx = None, stopping=False): checkSyncThreadIsAlive() @@ -68,20 +75,18 @@ async def postCurrentlyListening(ctx = None, stopping=False): if (current.playing is None or current.playing.id != newMetaData.id): #This function is only for logging current.playing = newMetaData - print('') - print(f"{current.playing} // {current.playing.id}", end ="") + logger.info(f"{current.playing} // {current.playing.id}") else: current.playing = newMetaData tempEmbed = nowPlayingEmbed(newMetaData) if stopping: #If stopping, send final update tempEmbed = nowPlayingEmbed(newMetaData, stopping=True) await current.message.edit(embed=tempEmbed.embed) - print('') #Newline so the "..." ends on a line break elif ctx != None: #If passed context (done when a new message is wanted), create new message current.message = await ctx.send(file=tempEmbed.rainwaveLogo, embed=tempEmbed.embed) else: #Otherwise, edit old message await current.message.edit(embed=tempEmbed.embed) - print('.', end ="") + logger.debug(f'Currently Listening updated {current.playing.id}') def formatSecondsToMinutes(incomingSeconds): minutes = str(incomingSeconds // 60) #get minutes, .zfill requires a string @@ -104,25 +109,25 @@ def generateProgressBar(metaData, stopping=False): timeSinceStartInSeconds = metaData.length #If time is too long, set max. else: #Else just do a seconds conversion. timeSinceStartInSeconds = times.timeSinceStart.seconds - if ((options.enableProgressBar == False - and options.enableProgressTimes == False) + if ((config["enableProgressBar"] == False + and config["enableProgressTimes"] == False) or stopping == True): return(None) - if options.enableProgressTimes == False: + if config["enableProgressTimes"] == False: timer = '' else: timer = f"`[{formatSecondsToMinutes(timeSinceStartInSeconds)}/{formatSecondsToMinutes(metaData.length)}]`" - progress = int(options.progressBarLength * (timeSinceStartInSeconds/metaData.length)) - if options.enableProgressBar == False: + progress = int(config["progressBarLength"] * (timeSinceStartInSeconds/metaData.length)) + if config["enableProgressBar"] == False: progressBar = '' - elif options.progressBarStyle == 1: #Left to right "fill" - progressBar = f"{options.progressBarCharacters[0] * progress}{options.progressBarCharacters[1] * (options.progressBarLength - progress)}" - elif options.progressBarStyle == 2: #Left to right indicator - progressBar = f"{options.progressBarCharacters[0] * (progress - 1)}{options.progressBarCharacters[1]}{options.progressBarCharacters[0] * (options.progressBarLength - progress)}" + elif config["progressBarStyle"] == 1: #Left to right "fill" + progressBar = f"{config["progressBarCharacters"][0] * progress}{config["progressBarCharacters"][1] * (config["progressBarLength"] - progress)}" + elif config["progressBarStyle"] == 2: #Left to right indicator + progressBar = f"{config["progressBarCharacters"][0] * (progress - 1)}{config["progressBarCharacters"][1]}{config["progressBarCharacters"][0] * (config["progressBarLength"] - progress)}" #TODO See if we can prevent the flickering from the formatting of Style3 - #elif options.progressBarStyle == 3: #Left to right color fill - # progressBar = f"```ansi\n{options.progressBarChars[0] * progress}{options.progressBarChars[0] * (options.progressBarLength - progress)}{timer}\n```" - if options.enableProgressBar == True and options.enableProgressTimes == True: + #elif config["progressBarStyle"] == 3: #Left to right color fill + # progressBar = f"```ansi\n{options.progressBarChars[0] * progress}{options.progressBarChars[0] * (config["progressBarLength"] - progress)}{timer}\n```" + if config["enableProgressBar"] == True and config["enableProgressTimes"] == True: spacer = ' ' else: spacer = '' @@ -132,7 +137,7 @@ def generateProgressBar(metaData, stopping=False): def nowPlayingEmbed(metaData, stopping=False): class formatedEmbed: syncThreadStatus = current.selectedStream._sync_thread.is_alive() - rainwaveLogo = discord.File("data/logo.png", filename="logo.png") + rainwaveLogo = discord.File("app/data/logo.png", filename="logo.png") if stopping: intro = 'Stopped playing' else: @@ -140,7 +145,7 @@ class formatedEmbed: embed = discord.Embed(title=f"{intro} Rainwave {metaData.album.channel.name} Radio", url=current.selectedStream.url, description=generateProgressBar(metaData, stopping), - color = discord.Colour.from_rgb(options.embedColor[0],options.embedColor[1],options.embedColor[2])) + color = discord.Colour.from_rgb(config["embedColor"][0],config["embedColor"][1],config["embedColor"][2])) if metaData.url: artistData = f"[{metaData.artist_string}]({metaData.url})" else: @@ -153,19 +158,19 @@ class formatedEmbed: async def validChannelCheck(ctx, checkVoiceChannel = False): response = True #Assume nothing is wrong - if (botChannels.restrictTextChannels #If disallowed channel - and (ctx.message.channel.id not in botChannels.allowedTextChannels)): + if (config["restrictTextChannels"] #If disallowed channel + and (ctx.message.channel.id not in config["allowedTextChannels"])): response = f"{bot.user.name} commands not allowed in {ctx.message.channel.name}" elif checkVoiceChannel == True: try: authorsChannel = ctx.message.author.voice.channel.id #Creating this variable checks that they're in a channel at all. - if (botChannels.restrictVoiceChannels #If disallowed voice channel - and (authorsChannel not in botChannels.allowedVoiceChannels)): + if (config["restrictVoiceChannels"] #If disallowed voice channel + and (authorsChannel not in config["allowedVoiceChannels"])): response = f"{bot.user.name} music playback not allowed in {ctx.message.author.voice.channel.name}" except: #If user not in a visible voice channel response = 'You do not appear to be in a voice channel' if response != True: #Log error and turn response into a bool for return - print(response) #TODO logging + logger.debug(f"Valid Channel Check: {response}") await ctx.message.channel.send(f"{ctx.message.author.mention} {response}") response = False return response @@ -185,16 +190,16 @@ async def stopConnection(): await setDefaultActivity() async def setDefaultActivity(): - await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=f" for `{options.botPrefix}play`")) + await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=f" for `{config["botPrefix"]}play`")) def loadOpus(): opusStatus = "Failed" if discord.opus.is_loaded() == False: try: - discord.opus.load_opus(dependencies.opus) + discord.opus.load_opus(config["opusLocation"]) opusStatus = "Initialized" except Exception as returnedException: - print(f"Opus loading error error: {returnedException}") + logger.warn(f"Opus loading error error: {returnedException}") else: opusStatus = "Pre-Loaded" return(opusStatus) @@ -207,12 +212,12 @@ def checkUserPresence(): break return(userPresent) -@tasks.loop(seconds = options.refreshDelay) #TODO Determine if 6 is actually safe, and if we can go lower +@tasks.loop(seconds = config["refreshDelay"]) #TODO Determine if 6 is actually safe, and if we can go lower async def updatePlaying(): usersPresent = checkUserPresence() if ((usersPresent == False) - and (options.autoDisconnect == True)): - print("All alone, disconnecting") + and (config["autoDisconnect"] == True)): + logger.info("All alone, disconnecting") await stopUpdates(gracefully = True) await stopConnection() else: @@ -224,12 +229,12 @@ async def on_ready(): current_day = now.strftime("%d/%m/%y") current_time = now.strftime("%H:%M:%S") opusStatus = loadOpus() - await bot.user.edit(username=options.botName) + await bot.user.edit(username=config["botName"]) loginReport = f'Logged into Discord as `{bot.user} (ID: {bot.user.id})` and Rainwave as `(ID: {rainwaveClient.user_id})` at `{current_time}` on `{current_day}`' - print(loginReport) - print(f"Opus: {opusStatus}") - if botChannels.enableLogChannel: - await bot.get_channel(botChannels.logChannel).send(loginReport) + logger.info(loginReport) + logger.debug(f"Opus: {opusStatus}") + if config["enableLogChannel"]: + await bot.get_channel(config["logChannel"]).send(loginReport) await setDefaultActivity() @bot.command(aliases=['p']) @@ -256,21 +261,21 @@ async def play(ctx, station = 'help'): await postCurrentlyListening(ctx) updatePlaying.start() elif (station.lower() == ('help' or 'list')): - await ctx.send(f"To start playback use `{options.botPrefix}play` followed by one of the available channels: {channelList}" - f"\nExample: `{options.botPrefix}play {channelList[0]}`") + await ctx.send(f"To start playback use `{config["botPrefix"]}play` followed by one of the available channels: {channelList}" + f"\nExample: `{config["botPrefix"]}play {channelList[0]}`") else: - await ctx.send(f"Station not found, use `{options.botPrefix}play help` for more info") + await ctx.send(f"Station not found, use `{config["botPrefix"]}play help` for more info") @bot.command(aliases=['leave','s']) ##, 'stop' async def stop(ctx): - """Stops radio""" + """Stops radio playback""" try: if current.voiceChannel.is_playing(): if (await validChannelCheck(ctx)): await stopUpdates() await stopConnection() except: - print("invalid stop command") + logger.info("invalid stop command") @bot.command(aliases=['np','whatson','wo','queue','q']) async def nowplaying(ctx, station = None): @@ -281,23 +286,24 @@ async def nowplaying(ctx, station = None): await postCurrentlyListening(stopping=True) await postCurrentlyListening(ctx) except: - print("invalid nowplaying command") + logger.info("invalid nowplaying command") -@bot.command() -async def test(ctx): - print('test') - -@bot.command() -async def check(ctx): - await ctx.send(len(ctx.message.author.voice.channel.members)) +#@bot.command() +#async def test(ctx): #Used for testing +# logger.info('test') @bot.command() async def ping(ctx): """Displays the bot's ping""" - print (f'Pong! {round(bot.latency * 1000)}ms') + logger.info(f'Pong! {round(bot.latency * 1000)}ms') await ctx.send(f'Pong! {round(bot.latency * 1000)}ms') -if options.refreshDelay < MINIMUM_REFRESH_DELAY: - options.refreshDelay = MINIMUM_REFRESH_DELAY - print(f"WARN refreshDelay overridden to: {MINIMUM_REFRESH_DELAY}") -bot.run(private.discordBotToken) +#Set up rainwave client +rainwaveClient = RainwaveClient() +rainwaveClient.user_id = login.rainwaveID +rainwaveClient.key = login.rainwaveKey + +if config["refreshDelay"] < MINIMUM_REFRESH_DELAY: + config["refreshDelay"] = MINIMUM_REFRESH_DELAY + logger.warning(f"WARN refreshDelay overridden to: {MINIMUM_REFRESH_DELAY}") +bot.run(login.discordBotToken) #Start Bot diff --git a/config/config.py.example b/config/config.py.example deleted file mode 100644 index 6d27ac2..0000000 --- a/config/config.py.example +++ /dev/null @@ -1,69 +0,0 @@ -class botChannels: - #To restrict which voice channels the bot can use, set below to true. - restrictVoiceChannels = False - #If above is set to true, list allowed channel/s IDs in a list as shown below. - allowedVoiceChannels = [123456789012345678, 987654321098765432] - - #To restrict which test channels the bot can receive commands on, set below to true. - restrictTextChannels = False - #If above is set to true, list allowed channel/s IDs in a list as shown below. - allowedTextChannels = [112233445566778899, 998877665544332211] - - #If you want the bot to log, login and error info in a channel, set below to True - enableLogChannel = True - #If above is set to true, list your logging channel ID below - logChannel = 246813579024681357 - -class private: - #Discord bot token - discordBotToken = 'F4K3T0K3N_ikb331nmGsvgHPGAv8jwFV3gKFs9eR.nF4lgje68ZdrEX9aSJ' - - #Rainwave API ID - rainwaveID = 12345 - - #Rainwave API Key - rainwaveKey = '12345abcde' - -class dependencies: - #Location of FFMPEG file. - ffmpeg = "ffmpeg-2021-11-22/bin/ffmpeg.exe" - - #Location of opus file. - opus = "" - -class options: - #Change the name of your bot here - botName = "Rain.Wave" - - #Change the prefix of your bot here - botPrefix = "rw." - - #Enables display of song progress in a numerical style timer e.g. [01:05/01:21] - enableProgressTimes = True - - #Enables display of song progress in a graphical manner e.g. ▰▰▰▱▱▱▱▱▱▱▱ - enableProgressBar = True - - #Allows selection of progress bar style between 1 and 2 - #1 is a left to right "fill" style progress bar e.g. ▰▰▰▱▱▱▱▱▱▱▱ - #2 is a moving indicator style progress bar e.g. ▱▱▱▰▱▱▱▱▱▱▱ - progressBarStyle = 1 - - #Allows selection of progress bar character length - progressBarLength = 14 - - #Allows selection of characters which make up the progress bar - #for style 1 ['▰','▱'] or ['▶','▷'] or ['█','░'] is suggested - #for style 2 ['═','╪'] or ['—','⎔'] or ['▬',':radio_button:'] is suggested - progressBarCharacters = ['▰','▱'] - - #Allows selection of embed sidebar color as an (r, g, b) value. - #You can use https://it-tools.tech/color-converter to generate an rgb color value - embedColor = (24, 135, 210) - - #Numer of seconds between refresh of progress bar and timer, can be increased if user is being rate limited. - #Minimum value can never be below 6. - refreshDelay = 6 - - #If set to `True`, bot will disconnect if no users are present in the bots voice channel. - autoDisconnect = True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 005ae42..58a21ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ #A list of dependencies for rainwavebot.py #The first three were found using `pipreqs`, is that a good choice? It did miss PyNaCl. -aiocron==1.8 +aiocron==1.8 #Required for discord.py discord.py==2.4.0 python_rainwave_client==2024.2 PyNaCl==1.5.0 #I got this by using `pip show pynacl`. If I hadn't known what would I do? +PyYAML==6.0.2 #Allows reading Yamls #Maybe more? \ No newline at end of file